Implementing Dark Mode in React via Context

Sandra Spanik - Mar 3 '21 - - Dev Community

One would think the timeline we live in is dark enough, but for some reason, developers are obsessed with giving users the option to make their lives even darker. In this article, we will go over how to implement toggling between dark and light mode in React. We'll also have a closer look at React Context, the tool we'll be using to achieve this.

Let's start with a simple React app with 6 components: a Button, a Navbar, as well as a nested "family" consisting of Grandparent, Parent, Child and Baby. Our top-most App component includes Navbar and GrandParent, and GrandParent in turn contains Parent, which contains Child, which contains Baby.



function App() {
  return (
    <>
      <Navbar />
      <div className="App-div">
        <GrandParent />
      </div>
    </>
  );
}


Enter fullscreen mode Exit fullscreen mode

Let's assume, for ease of variable naming, that it's perfectly logical for a child to have a baby, and for this baby to have a grandparent instead of a great-grandparent. Now that our disbelief is suspended, let's look at what this might look like in light mode below.

Screenshot showing nested components in light mode

Sidenote: the monstrosity above acts as a nice segue into letting you know that clearly, the focus of this article will not be CSS, but the implementation of the React logic which enables us to easily switch between CSS classes across all components. If you're looking for aesthetically pleasing dark mode CSS, keep looking, and best of luck to you.

The aim is to eventually end up in a place where the user can toggle between the current light mode and dark mode by simply clicking on a toggle switch or icon in the Navbar.

Step 1: Add Toggle Switch / Icon

Toggle switches are actually heavily styled inputs of the type checkbox. Nobody implements toggle switches from scratch. Nobody. Not a single person. Unless they like CSS, which I hear can be the case for a select few 馃槸 So let's grab some code, for example from here, and add our switch to the navbar. You could also instead add buttons wrapped around sun/moon icons, for example from here. Our page now looks like this:

Light theme with added toggler switch

Beautiful!

Step 2: Share Data Between Components

To implement dark mode, we'll need to find a way to share data between components efficiently. Let's say that in our example, the GrandParent component wanted to share something with the Baby component. One way to do this would be to define a variable or piece of state at the GrandParent level and pass it down via the Parent and Child components all the way to the Baby component, like so:

The GrandParent defines the variable and passes it down to the Parent.



const GrandParent = () => {
  const grandmasFavSong = "Toxic by B. Spears";
  return (
    <div className="GrandParent-div">
      <Parent grandmasFavSong={grandmasFavSong} />
      <div>I AM THE GRANDPARENT 馃懙  and my fav song is {grandmasFavSong}</div>
      <Button />
    </div>
  );
};


Enter fullscreen mode Exit fullscreen mode

The Parent destructures the grandmasFavSong prop and passes it down to Child. Arduous...



const Parent = ({ grandmasFavSong }) => {
  return (
    <div className="Parent-div">
      <Child grandmasFavSong={grandmasFavSong} />
      <div>I AM THE PARENT 馃懇</div>
      <Button />
    </div>
  );
};


Enter fullscreen mode Exit fullscreen mode

The Child now also has to destructure the prop and pass it down to the Baby component. 馃ケ馃ケ馃ケ



const Child = ({ grandmasFavSong }) => {
  return (
    <div className="Child-div">
      <Baby grandmasFavSong={grandmasFavSong} />
      <div>I AM THE CHILD 馃 </div>
      <Button />
    </div>
  );
};


Enter fullscreen mode Exit fullscreen mode

Finally, the Baby component knows of Grandma's secret obsession.



const Baby = ({ grandmasFavSong }) => {
  return (
    <div className="Baby-div">
      <div>
        I AM THE BABY 馃嵓  why is grandma making me listen to {grandmasFavSong}??
      </div>
      <Button />
    </div>
  );
};


Enter fullscreen mode Exit fullscreen mode

Prop Drilling demo

You might have noticed that this is not a very elegant way of achieving data sharing between components. Doing this is known as prop drilling. It's considered bad practice and should be avoided, much like its cousins oil drilling & tooth drilling. Best to just avoid drilling of any kind. Thankfully, React provides a neat alternative.

Enter React Context.

In life as well as in React, context is key. React Context provides a way to share data between components without having to pass it down as a prop through each level of the component hierarchy. Using said React Context is a better way to share grandma's unhealthy obsession with 2000s pop than what we've seen above. The workflow is as follows:

  1. Create Context
  2. Add the data intended to be shared to Context
  3. Wrap a Context provider around the components that need access to it
  4. Consume the Context provider wherever needed

Let's go through this step by step.

1. Create Context

We'll do this in a new file called MusicContext.js:



import React from "react";

export default React.createContext();


Enter fullscreen mode Exit fullscreen mode

That's all? Yep, that's all.

2. Add the data intended to be shared to Context

Let's create a new file called MusicProvider.js. We'll define our data here, and use the children prop to make sure every component the MusicProvider is wrapped around has access to our values.



import React from "react";
import MusicContext from "./MusicContext";

const MusicProvider = ({ children }) => {
  const grandmasFavSong = "Toxic by B. Spears";
  return (
    <MusicContext.Provider value={grandmasFavSong}>
      {children}
    </MusicContext.Provider>
  );
};
export default MusicProvider;


Enter fullscreen mode Exit fullscreen mode
3. Wrap the Context provider around relevant components

In this case, we don't need our Navbar to have access to the data, but we do want GrandParent and Baby to have access. And so, we'll wrap the provider around GrandParent, within which all the other Family components are nested.



import MusicProvider from "./Context/MusicProvider";

function App() {
  return (
    <>
      <Navbar />
      <div className="App-div">
        <MusicProvider>
          <GrandParent />
        </MusicProvider>
      </div>
    </>
  );
}


Enter fullscreen mode Exit fullscreen mode
4. Consuming Context wherever needed

We want to display the data in the GrandParent and Baby components. We'll need to take the following steps in each of the files:

  1. Import the useContext hook from React
  2. Import MusicContext (not MusicProvider)
  3. Extract the variable out of Context

Let's look at how to do this is the Baby component:



import React, { useContext } from "react";
import "./Family.css";
import Button from "./Button";
import MusicContext from "../Context/MusicContext";

const Baby = () => {
  // extracting variable from context 猬囷笍猬囷笍
  const grandmasFavSong = useContext(MusicContext);
  return (
    <div className="Baby-div">
      <div>
        I AM THE BABY 馃嵓聽聽why is grandma making me listen to {grandmasFavSong}??
      </div>
      <Button />
    </div>
  );
};

export default Baby;


Enter fullscreen mode Exit fullscreen mode

After doing the same for GrandParent, our app should look as it did before. Whilst it's not immediately obvious that this is a more efficient way of sharing data between components than prop drilling in our tiny app, trust me when I tell you that the utility of using Context scales with application size and number of components.

What about dark mode?

Now that we understand React Context, let's use it to implement dark mode. There's many ways to achieve this, but here we'll use the class dark聽and associate it with dark-mode styling in our CSS. The class dark will be rendered in relevant components conditionally using the ternary operator. Let's use our Button component as an example:



import React from "react";
import "./Button.css";

const Button = () => {
let darkMode = isDark ? "dark" : "";
  return (
    <button className={`Button-btn ${darkMode}`}>
      {isDark ? "Dark" : "Light "} button
    </button>
  );
};

export default Button;


Enter fullscreen mode Exit fullscreen mode

Now, let's follow through the same steps as when we were handling music Context.

1. Create The Context in ThemeContext.js:


import React from "react";

export default React.createContext();


Enter fullscreen mode Exit fullscreen mode
2. Add values to the Context provider

We'll define our state, isDark, in a file called ThemeProvider.js. We will also define a function that toggles isDark. Both will be passed to the provider's children as Context values. This time, since we have more than one value, we'll wrap them in an object.



import React, { useState } from "react";
import ThemeContext from "./ThemeContext";

const ThemeProvider = ({ children }) => {
  const [isDark, setIsDark] = useState(false);
  const toggleMode = () => {
    setIsDark((mode) => !mode);
  };

  return (
    <ThemeContext.Provider value={{ isDark, toggleMode }}>
      {children}
    </ThemeContext.Provider>
  );
};

export default ThemeProvider;


Enter fullscreen mode Exit fullscreen mode
3. Wrap the Context provider around relevant components

This time, we'll want to wrap it around all components, including our Navbar.



import "./App.css";
import GrandParent from "./Family/GrandParent";
import "./Family/Family.css";
import Navbar from "./Navbar/Navbar";
import MusicProvider from "./Context/MusicProvider";
import ThemeProvider from "./Context/ThemeProvider";

function App() {
  return (
    <ThemeProvider>
      <Navbar />
      <div className="App-div">
        <MusicProvider>
          <GrandParent />
        </MusicProvider>
      </div>
    </ThemeProvider>
  );
}

export default App;


Enter fullscreen mode Exit fullscreen mode
4. Consuming Context wherever needed

Let's again use our Button component as an illustration:



import React, { useContext } from "react";
import "./Button.css";
import ThemeContext from "../Context/ThemeContext";

const Button = () => {
  const { isDark } = useContext(ThemeContext);
  let darkMode = isDark ? "dark" : "";
  return (
    <button className={`Button-btn ${darkMode}`}>
      {isDark ? "Dark" : "Light "} button
    </button>
  );
};

export default Button;


Enter fullscreen mode Exit fullscreen mode

After following a similar approach in each component that we want to be affected by the mode changing, the only thing left to do is to implement its toggling. We're already sharing the toggle function via Context, so let's grab it where we need it: in the ToggleSwitch component. We'll create an event that fires on click and triggers the mode toggling.



import React, { useContext } from "react";
import "./ToggleSwitch.css";
import ThemeContext from "../Context/ThemeContext";

const ToggleSwitch = () => {
  const { toggleMode, isDark } = useContext(ThemeContext);

  return (
    <div className="ToggleSwitch-div">
      <label className="switch">
        <input onClick={toggleMode} type="checkbox" />
        <span class="slider round"></span>
      </label>
    </div>
  );
};

export default ToggleSwitch;


Enter fullscreen mode Exit fullscreen mode

Rejoice! 馃憦馃憦馃憦 We're done. Now our app looks like this, or indeed much much better, depending on how much effort we put in our CSS.

GIF of toggling between light and dark modes

. . . . . . . . . . . . .