3

My website has a light and dark theme. The default theme is light. If the user changes the theme to dark, it is saved to localStorage.

On the next visit/refresh at the root of my component tree, this code runs:

  useLayoutEffect(() => {
    let storedTheme = localStorage.getItem("theme");

    if (storedTheme === "light" || storedTheme === "dark") {

    // Redux action. Other components subscribe to the theme.
      setTheme(storedTheme);
    }
  }, [setTheme]);

Say the user chose the dark theme as their preference. It works fine. However, the first render will be the light theme. The second render will be the dark theme. This causes a light to dark flicker when visiting the site.

Is there a way to ensure my first render has the set value from localStorage?

Osama Qarem
  • 1,359
  • 2
  • 13
  • 15

3 Answers3

1

One of a workaround to remove flicker is by conditional rendering. You could render an empty page on the first render, then render the content with black mode style on the second render. The condition could be checked by whether window object is ready e.g..

function MyApp() {
    const [theme, setTheme] = useState(null);

    useEffect(() => {
        let theme = localStorage.getItem('theme') || 'light';
        setTheme(theme);
    }, []);

    if (!theme) {
        return; // `theme` is null in the first render
    }

    return (
        <Component {...pageProps} />
    );
}

This workaround could also reduce the time of Cumulative Layout Shift in some cases (font style different between two renders etc.)

p.s. As I put both light and dark CSS code inside JS code (in memory), which will avoid slow file switching, makes re-render even faster.

Here is Another solution, in className approach.

And Another solution but with long Speed Index time (using Google Lighthouse)

0

Implement theme saving to local storage as follows.

const [storedTheme, setStoredTheme] = useState(JSON.parse(localStorage.getItem("theme")) || "light");


useEffect(() => {
    localStorage.setItem("theme", JSON.stringify(storedTheme));
}, [storedTheme]);

And then in the App UI use method setStoredTheme() to set theme to "light" or "dark".

Janiis
  • 1,478
  • 1
  • 13
  • 19
  • I already do this in my app. But it is not enough. If you know of a way to initalize the redux store from `localStorage` or `window` object, then please let me know. I think that might be the solution. I can't seem to get that to work because my site is static and `localStorage` or `window` are not defined at build time. – Osama Qarem Jul 03 '19 at 02:22
0

What worked in getting data to my first render was initializing the redux store like so:

if (typeof localStorage != "undefined") {
  initialState = {
    theme: localStorage.getItem("theme"),
  };
} else {
  initialState = {};
}

const store = createStore(reducers, initialState);

But it worsened the situation as the components no longer re-rendered on first load because the theme prop from redux never changed because the store was initialized with the user preference. So I went back to the old approach with in the original question above.

The flicker happens because I am using Javascript to control the theme in my react app. I am also using Gatsby which generates a static version of my site that loads before my react app.

Loading the site goes like this:

  1. E.g. User with dark theme preference visits. They see static content without any Javascript while JS bundles download in the background.

  2. Since light theme is the default, the static version of the site will be light.

  3. Javascript loads and React's first render happens and the redux store is intialized. The root component gets the theme from localStorage and the tree re-renders with the dark theme.

The flicker happens because of the events between 2 and 3.

I didn't manage to solve the issue completely. But I managed to alleviate the flicker to be much more unnoticable by enabling Gatsby's service worker. The worker caches Javascript bundles for next site visit, and loading Javascript from disk means the flicker will linger for a shorter amount of time as the JS bundles are already available for execution.

Osama Qarem
  • 1,359
  • 2
  • 13
  • 15
  • Coming back to this question a long time later, removing the flicker can be achieved by: 1. handling the theme with css classes instead of letting React re-render with the correct theme and 2. Setting the css class for light or dark based on user preference in a script tag that loads before the app does. This way the app’s first render will have the correct theme. Further reading: https://www.joshwcomeau.com/react/dark-mode/ – Osama Qarem Oct 21 '22 at 06:44