DEV Community

loading...
Cover image for I Used React Context To Enable Dark Mode

I Used React Context To Enable Dark Mode

sanliverpool13
Striving to learn more everyday
・4 min read

I published this article on Medium as well

It seems to me that dark mode is offered everywhere nowadays. And it feels good on the eye. So I added the option to my website.

Many websites I browse offer the dark mode option, and sooner than later I switch to it. Both my WhatsApp and Notion are in dark mode.

I decided to make sure my personal website offered a darker theme as well.

Approach

The idea was to keep the theme in the state of some parent React component, for instance the App component. A function to toggle this state defined as toggleDark was passed down the tree to the button component.

The theme state variable was also passed down the tree, and every time the theme state toggled, components re-rendered with a new theme style.

However, I had to pass the theme state as a prop through multiple levels of the React tree. This meant many intermediary components did not utilize the theme state prop at all. It seemed wasteful. I pondered what would have happened had I implemented this with a larger React component tree.

Thus I wanted only the components in need of the theme state to have access to it. I decided to use React Context.

I could have implemented the Redux Store as an alternative, but my application was not that sizable that it needed a whole store to maintain the state of the application. React Context seemed like the perfect solution for the size and complexity of my application.

React Context

First I had to create the Context object. It was initialized with a default state.

    const defaultState = {
       dark: false,
       toggleDark: () => {}
    }
    const ThemeContext = React.createContext(defaultState);
Enter fullscreen mode Exit fullscreen mode

For every such React context object, there is context provider component.

    <ThemeContext.Provider value={{dark, toggleDark}}>
       {children}
    </ThemeContext.Provider>
Enter fullscreen mode Exit fullscreen mode

The value of this context provider is available to all the children components that consume the context, and every time this value updates, the consuming components re-render.

How do the children components consume the context? In the case of functional components, they subscribe to the context via the useContext hook. Every child component of the ThemeContext.

Provider that subscribes to the context receives its context state from the value prop.
For example the Button component I wrote consumed the context as follows:

    const Button = () => {
       const contextState = React.useContext(ThemeContext);
       return(
         // Jsx Here
       )
    }
Enter fullscreen mode Exit fullscreen mode

When react rendered this component it read the current context state from the closest matching Provider parent of ThemeContext. If there were no matching parent Providers the default context value was set.

Codepen Example

I created a default context state, and the context object as the first step.

   const defaultState = {
     dark: false,
     toggleDark: () => {},
   }
   const ThemeContext = React.createContext(defaultState);
Enter fullscreen mode Exit fullscreen mode

I then had to create a custom ThemeProvider component.

   const ThemeProvider = ({children}) => {
      const [dark, setDark] = React.useState(false);

      const toggleDark = (e, dark2) => {

        let dark = !dark2
        setDark(dark)

      }


      return (
        <ThemeContext.Provider value={{dark, toggleDark}}>
          {children}
        </ThemeContext.Provider>
      )
   }
Enter fullscreen mode Exit fullscreen mode

This was the context theme provider, however I added the dark state to it to keep a reference to the theme. I also defined the function that would toggle the theme by invoking setDark . I provided dark and toggleDark to the children that were to consume the context in the value prop of ThemeContext.provider.

I then included this custom context provider in the main parent App component.

const App = () => {

    return(
      <div className="app">
        <div className="app-center">
          <ThemeProvider>
            <Navbar>
              <Button/>
            </Navbar>
            <Content/>
          </ThemeProvider>
        </div>
      </div>
    )

}
Enter fullscreen mode Exit fullscreen mode

Navbar, Button and Content components all subscribed to the context using the useContext hook.

const Button = () => {
   const {dark, toggleDark} = React.useContext(ThemeContext);
   return (
      <button className="button" onClick={e => toggleDark(e,dark)}>
       Toggle Theme
      </button>
   )
}
const Navbar = () => {
  const {dark} = React.useContext(ThemeContext);
  return(
    <nav className={dark ? "navbar-dark" : "navbar"}>
      {children}
    </nav>
  )
}
const Content = () => {
   const {dark} = React.useContext(ThemeContext);
      return(
        <div className={dark ? "content-dark" : "content"}>
          <h1>Content</h1>
          <h4>Will Consume React Context</h4>
          <p>Once the toggle theme button is pressed, the theme   value in the React Context object will change, and accordingly this content will change its theme</p>
        </div>
      )
}
Enter fullscreen mode Exit fullscreen mode

The button needed access to the toggle function to toggle the theme of the application, while the navigation bar and the content components only needed to subscribe to the darkstate and render the corresponding css style.

(***)

As you can see no props were passed down from the component that held the theme state to the components that needed the theme state. When a component required access to the theme state, it simply subscribed to the context and got access to the theme state.

I realize that for my example on Codepen you might argue why I even bothered to use React Context if there were only 2 or 3 levels of components. But I just wanted to share the logic and implementation of React Context in as simple a fashion as I could. My implementation of React Context for my personal website was more justified, as I had many more components throughout the React tree, that independently required access to the theme state, while many intermediary components were not even aware of the state.

(***)

Please let me know if I made any mistakes and if there are simpler ways to implement React Context or maybe not use React Context at all and have a simpler solution in place. Thank you for reading this article!

Discussion (2)

Collapse
frontendphil profile image
Philipp Giese • Edited

Hey there :) Every time I see a function that is called toggleSomething and then accepts a parameter I make this comment. I'd either rename it to setSomething if it accepts a parameter or remove the need for the param instead. In your example you could achieve it like this:

const toggleDark = useCallback(() => {
  setDark((dark) => !dark)
}, [])
Enter fullscreen mode Exit fullscreen mode

FYI I've used useCallback so that the function does not change every time the context provider renders. This way consumers do not need to update necessarily even though nothing has changed.

Collapse
sanliverpool13 profile image
sanliverpool13 Author

Hey Philipp, thank you for the tip! Your code is much cleaner and makes much more sense for that function. Duly noted! And yes I understand your use of useCallback, so that the function is not redefined on every render of the component. Thank you for taking your time to read my article. Appreciate it!