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>
</>
);
}
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.
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:
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>
);
};
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>
);
};
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>
);
};
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>
);
};
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:
- Create Context
- Add the data intended to be shared to Context
- Wrap a Context provider around the components that need access to it
- 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();
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;
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>
</>
);
}
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:
- Import the useContext hook from React
- Import MusicContext (not MusicProvider)
- 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;
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;
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();
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;
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;
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;
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;
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.