Implement Dark/Light mode: How to fix the flicker of incorrect theme?
Tags: nextjs, javascript, webdev, react
Published: 2022-08-21 12:16:00

Some time ago, I created my portfolio website using `React + Next.js`. I also added a dark mode toggle switch. 💡

Recently, I found some free time to look at the functionality again. The switch works well but the initial load suffers from a problem. There is a **flash of incorrect theme** when the page loads for a very small time. The flash can be more noticeable on different devices and network connections.

<figure>
<img width="100%" style="width:100%" src="https://media.giphy.com/media/DbXv45mt03aXpW9bhE/giphy.gif"><figcaption>Webpage loading in a throttled environment</figcaption>
</figure>

Below is a write up of how I fixed it for my particular case.

The article does not go over the basics of how to create a dark mode switch using `React` (and/or `Next.js`) with `localStorage`. There are other brilliant articles for that. This article is just a write up showing how one would build on their existing approach to tackle the flicker problem. My portfolio is built on `Next.js`, but I think a similar approach can be used for other server side frameworks like `Gatsby`.

This article assumes that the reader has basic knowledge of `React Context` and `Next.js`. I have tried to link to the docs wherever possible.
 
## Table of Contents

1. [Theme switcher using local storage and context](#usingLocalStorage)
2. [The flicker problem](#flickerProblem)
3. [Using Lazy State Initialisation](#lazyStateWithStorage)
4. [Using cookies](#usingCookies)
5. [Customising the Document file](#usingDocument)
6. [Summary](#summary)



## Theme switcher using local storage and context <a name="usingLocalStorage"></a>

First things first. Here is a basic outline of the initial approach. 

The theme is powered by React Context. The user preference is saved in `localStorage`. The changes are made using [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties). 

Here is what context looks like: 
```
const Context = createContext({
  theme: "",
  toggleTheme: null
});
```
An object containing `theme` value and a method to modify it. Now any component that consumes this context can read the theme value (and modify it, if need be).

The CSS variables are stored in a constants file.

```
export const colorPalette = {
  dark: {
    background: "#222629",
    paraText: "#fff",
    headerText: "#fff",
    base: "#fff",
    pressed: "#c5c6c8",
    shade: "#2d3235"
  },
  light: {
    background: "#fffff",
    paraText: "#15202b",
    headerText: "#212121",
    base: "#212121",
    pressed: "#22303c",
    shade: "#f5f5f5"
  }
};

export const filter = {
  dark: {
    socialMediaIcon:
      "invert(100) sepia(0) saturate(1) hue-rotate(0deg) brightness(100)"
  },
  light: {
    socialMediaIcon: "invert(0) sepia(0) saturate(0) brightness(0)"
  }
};
```
The `colorPalette` is self explanatory. The `filter` variable is where filters are stored. 

### Why filter for images? 🏞

It is very likely, that one would want to show logos/images in a different colour for different themes. A trick to do that is by using CSS filters which can change the logo colors🎨. (My website is monotone so it was much easier to convert the icons to black and white). This way the page does not have to make request to a new image. On noticing the above GIF, one can see green logos (their original colour) initially, which turn black and white.

Below is the function that changes the colour palette and the filters based on the input theme:

```
const changeColorsTo = (theme) => {

  const properties = [
    "background",
    "paraText",
    "headerText",
    "base",
    "pressed",
    "shade"
  ];

  if (typeof document !== "undefined") {
    properties.forEach((x) => {      document.documentElement.style.setProperty(
        `--${x}`,
        colorPalette[(theme === undefined ? "LIGHT" : theme).toLowerCase()][x]
      );
    });
    document.documentElement.style.setProperty(
      `--socialIconsfilter`,
      filter[(theme === undefined ? "LIGHT" : theme).toLowerCase()]
        .socialMediaIcon
    );
  }
};
```

[setProperty](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration/setProperty) is used to set the CSS variables.
 
Below is the ContextProvider, which wraps all elements on the webpage. 

```
const ContextProvider = (props) => {

  let [currentTheme, setTheme] = useState("LIGHT");

  useEffect(() => {
    let storageTheme = localStorage.getItem("themeSwitch");
    let currentTheme = storageTheme ? storageTheme : "LIGHT";
    setTheme(currentTheme);
    changeColorsTo(currentTheme);
  }, []);

  let themeSwitchHandler = () => {
    const newTheme = currentTheme === "DARK" ? "LIGHT" : "DARK";
    setTheme(newTheme);
    window && localStorage.setItem("themeSwitch", newTheme);
    changeColorsTo(newTheme);
  };

  return (
    <Context.Provider
      value={{
        theme: currentTheme,
        toggleTheme: themeSwitchHandler
      }}
    >
      {props.children}
    </Context.Provider>
  );
};

export { Context, ContextProvider };
```

The `currentTheme` is initialised with `LIGHT`. After the first mount, the **correct** theme value is read from `localStorage` and updated accordingly. If localStorage is empty, then LIGHT is used. 
The `themeSwitchHandler` function is called to change the theme. It performs three actions:
1. Updates the `CSS variables` by calling `changeColorsTo`,
2. updates the `localStorage` value, and
3. sets the new value for `currentTheme`, so the context value is also updated.

The below is the code for [`_app.js`](https://nextjs.org/docs/advanced-features/custom-app). With Next.js, one can use a custom `App` component to keep state when navigating pages (among other things).

```
const MyApp = ({ Component, pageProps }) => {

  return (
    <>
      <Head>
        ....
        <title>Tushar Shahi</title>
      </Head>
      <ContextProvider>
        <Layout>
          <Component {...pageProps} />
        </Layout>
      </ContextProvider>
    </>
  );
};
```

The relevant part is how `ContextProvider` wraps all the components.

## The flicker problem<a name="flickerProblem"></a>

The above code gives a hint as to why there is a flickering problem. Initially there is no information about the user preference. So LIGHT is used as the default theme, and once `localStorage` can be accessed, which is inside the useEffect callback (`useEffect` with any empty dependency array works like `componentDidMount`), the correct theme is used. 

### How to initialise the state correctly?
An update to the code could be done by utilising [lazy initial state](https://reactjs.org/docs/hooks-reference.html#lazy-initial-state).

```
const setInitialState = () => {

  let currentTheme = "LIGHT";

  if (typeof window !== "undefined" && window.localStorage) {
    let storageTheme = localStorage.getItem("themeSwitch");
    currentTheme = storageTheme ? storageTheme : "LIGHT";
  }

  changeColorsTo(currentTheme);
  return currentTheme;
};

const ContextProvider = (props) => {
  let [currentTheme, setTheme] = useState(setInitialState);
.....
```

`setInitialState` reads the theme value, changes the color and returns the theme. Because Next.js renders components on the server side first, `localStorage` cannot be accessed directly. The usual way to ensure such code runs only on client side is by checking for this condition: 
```
typeof window !== "undefined"
``` 

This does not help though. Again, there is a flicker. On top of that there is a [`hydration error`](https://nextjs.org/docs/messages/react-hydration-error). 
`Warning: Text content did not match. Server: "LIGHT" Client: "DARK"` in `ModeToggler` component.

<figure>
<img width="100%" style="width:100%" src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/68f95auw1y3kwdiccp2n.png"><figcaption>Hydration error in development environment</figcaption>
</figure>

**The issue:** Server side value of `theme` is `LIGHT` and client side it is `DARK`. Understandable because `localStorage` is not available server side. This value is rendered as `text` in the `ModeToggler` component, hence the mismatch.

## Using cookies<a name="usingCookies"></a> 🍪

The network tab shows the value of theme in the HTML page being served is incorrect.

<figure>
<img width="100%" style="width:100%" src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h1voketgfy00bkufvtt1.png"><figcaption>Network tab inspection</figcaption>
</figure>

To fix this, a data store which is accessible to both client and server needs to be used. `cookies` is the way. And with Next.js data fetching methods it becomes easy to access them.

Implementing [`getServerSideProps`](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props) on relevant pages does this:

```
export const getServerSideProps = async ({ req }) => {

  const theme = req.cookies.themeSwitch ?? "LIGHT";

  return {
    props: {
      theme
    } // will be passed to the page component as props
  };
};
```
The above code runs on every request.

`theme` is made used in the `MyApp` component.

```
const MyApp = ({ Component, pageProps }) => {

      return(
      ....
      <ContextProvider theme={pageProps.theme}>
        <Layout>
          <Component {...pageProps} />
        </Layout>
      </ContextProvider>
      ....
```

Now, the prop `theme` is used to initialise the state in the `ContextProvider`. 

```
const ContextProvider = ({ theme, children }) => {

  let [currentTheme, setTheme] = useState(() => {
    changeColorsTo(theme);
    return theme;
  });

  let themeSwitchHandler = () => {
    const newTheme = currentTheme === "DARK" ? "LIGHT" : "DARK";
    setTheme(newTheme);
    changeColorsTo(newTheme);
    if (document) document.cookie = `themeSwitch=${newTheme}`;
  };

  return (
    <Context.Provider
      value={{
        theme: currentTheme,
        toggleTheme: themeSwitchHandler
      }}
    >
      {children}
    </Context.Provider>
  );
};
```

The code using `localStorage` is replaced by the code using `cookies`. Now the information about correct theme is present on the server side too. Inspecting the network tab confirms that.

<figure>
<img width="100%" style="width:100%" src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/glvk4grbonv9i6kemzik.png"><figcaption>Correct theme value from server side</figcaption>
</figure>


**But** there is still a flicker. 😩

<figure>
<img width="100%" style="width:100%" src="https://media.giphy.com/media/Wrm8r47WjJrizS7N90/giphy.gif"><figcaption>Webpage loading in a throttled environment</figcaption>
</figure>

The function `changeColorsTo` has a check for the existence of `document` so that the code manipulating the colors only runs on client side. The loaded `html` file shows that the styles are not loaded from the server side. This indicates that the client side code (not the server side code) updates all the CSS variables, even if the correct value of theme is available on the server side.

<figure>
<img width="100%" style="width:100%" src="https://media.giphy.com/media/cDBQPHATx4BuQkIW30/giphy.gif">
<figcaption>Elements Panel in Dev Tools</figcaption>=
</figure>

How to utilise the cookie info to add the styles on server side?

## Customising the Document file<a name="usingDocument"></a>

[`_document.js`](https://nextjs.org/docs/advanced-features/custom-document) is used in Next.js to update the `html` and `body` tags. The file runs on the server side. It is a good place to load fonts and any scripts (both inline and remote). 

Document component can implement a [`getIntialProps`](https://nextjs.org/docs/api-reference/data-fetching/get-initial-props). This is also a data fetching method. It has access to `context` and `request`. This is where one can access the `themeSwitch` cookie and pass it on as a prop.

```
MyDocument.getInitialProps = async (ctx) => {

  const initialProps = await Document.getInitialProps(ctx);
  const theme = ctx.req?.cookies?.themeSwitch ?? "LIGHT";

  return { ...initialProps, theme };
};
```

The `Document` component can read the theme and create the styles object. This will be added to the `html` tag. Now every time any page is served, the html styles will be filled directly by the server.

### Why optional chaining to access cookies?
There is a need for the [optional chaining operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) because `getInitialProps` runs for every page served. And `404` pages do not have data fetching methods like `getServerSideProps` or `getInitialProps`. `req` object does not exist for `404.js` and hence accessing `cookies` will throw an error.

```
const MyDocument = ({ theme }) => {

    const styleObject = useMemo(() => {
    let correctTheme =
      colorPalette[(theme === undefined ? "LIGHT" : theme).toLowerCase()];
    let correctFilter =
      filter[(theme === undefined ? "LIGHT" : theme).toLowerCase()];

    const styles = {};

 Object.entries(correctTheme).forEach(([key, value]) => {
      styles[`--${key}`] = value;
    });
    styles[`--socialIconsfilter`] = correctFilter.socialMediaIcon;
    return styles;
  }, [colorPalette, filter]);

  return (
    <Html lang="en" style={styleObject}>
      <Head>
      ....
      </Head>
      <body>
        <Main />
        <NextScript />
        .... 
      </body>
    </Html>
  );
};
```

The component body creates a `stylesObject` using the correct theme with the `colorPalette` and `filter` object. 

<figure>
<img width="100%" style="width:100%" src="https://media.giphy.com/media/4HoCan5e0fpgu3nzwu/giphy.gif"><figcaption>Webpage loading in a throttled environment</figcaption>
</figure>

**Yes. There is no flicker now.** 🎉  The website is flicker-less.

The network tab shows that the CSS variables are being pre filled when the page is served.

<figure>
<img width="100%" style="width:100%" src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mc8mcsil30zq4jl9xtws.png"><figcaption>Network tab inspection</figcaption>
</figure>

With this set, the context code can be updated. Now it is not required to change colors on the first render. So there is no need to have a function in `useState`.

```
const ContextProvider = ({ theme, children }) => {
  let [currentTheme, setTheme] = useState(theme);
```

## Summary<a name="summary"></a>

1. There is a need to use `cookies` instead of `localStorage` because information is needed both on client and server side.
2. Theme can be read from cookies in data fetching methods and passed as props to all the pages.
3. Updating the `CSS variables` using `Context` will still cause a flicker because the server rendered page is served with the wrong colors.
4. To get the correct value in `CSS variables` Next.js's `Document` component is customised. It can update the `body` & the `html` and is run on the server side.


The code is deployed on [vercel](https://tusharshahiportfolio.vercel.app/). One might notice that the `404` page does not get the correct theme, because of the implementation.
 
Hope this is helpful to people reading this.