Adding Dark mode to your React app with Context API and the useReducer Hook
Managing state in React can be quite tricky. Libraries like Redux make this more manageable as they only keep one source of truth (the store) that you interact with. In many cases though, this can be too complex to set up. In this tutorial I will show you how to use the React Context API together with the useReducer hook to avoid manually passing down props.
You can read more on the useReducer hook here. And here you can find more on Context API.
1. Setup
Let's set up a basic React scene with a Navbar component. I'm using styled components for the CSS, where we will pass our theme too later in the tutorial. I've added some global styles and some styled divs for the Navbar.
index.js
import React from "react";
import ReactDOM from "react-dom";
import { createGlobalStyle } from "styled-components";
import Nav from "./Nav";
function App() {
return (
<>
<GlobalStyles />
<Nav />
<h1>Hi Dev.to!</h1>
</>
);
}
const GlobalStyles = createGlobalStyle`
html, body {
padding: 0;
margin: 0;
box-sizing: border-box;
background: #e5e5e5;
font-family: sans-serif;
}
`;
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Nav.js
import React from "react";
import styled from "styled-components";
export default function Nav() {
return (
<NavBar>
<NavMenu>
<NavLink>Home</NavLink>
<NavLink>About Us</NavLink>
<NavLink>Contact</NavLink>
</NavMenu>
<NavToggle>Toggle theme</NavToggle>
</NavBar>
);
}
const NavBar = styled.div`
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
background: #333;
min-height: 50px;
font-size: 1.2rem;
font-weight: 500;
color: white;
list-style: none;
`;
const NavMenu = styled.div`
display: flex;
align-items: center;
justify-content: flex-start;
`;
const NavLink = styled.div`
display: block;
padding: 1rem;
transition: 250ms ease background-color;
&:hover {
cursor: pointer;
background-color: skyblue;
}
`;
const NavToggle = styled(NavLink)`
text-decoration: underline;
`;
2. Adding ThemeProvider
Next up we will add the ThemeProvider wrapper component from styled-components, which is a HOC components that makes use of React's Context API by proving the theme available in all the components it wraps.
Inside index.js:
import { createGlobalStyle, ThemeProvider } from "styled-components";
...
function App() {
return (
<ThemeProvider theme={currentTheme}>
<GlobalStyles />
<Nav />
<h1>Hi Dev.to!</h1>
</ThemeProvider>
);
}
...
3. Adding state
Now we will use the useReducer hook to define our state and dispatch actions to modify our current state which holds the theme we want to show the user.
Inside index.js:
const [state, dispatch] = useReducer(reducer, initialState);
const { currentTheme } = state;
Next up we will create a new context for our app which will hold the state and dispatch function. Then we'll wrap this context around our app so we can acces it from every component
...
export const AppContext = createContext();
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const { currentTheme } = state;
return (
<ThemeProvider theme={currentTheme}>
<AppContext.Provider value={{ ...state, dispatch }}>
<GlobalStyles />
<Nav />
<h1>Hi Dev.to!</h1>
</AppContext.Provider>
</ThemeProvider>
);
}
...
4. Adding reducer
We will create a reducer.js file to store the reducer function and initial state. I will set this theme to dark as the initial theme. The reducer updates our state based on the action type it receives.
import { theme } from "./theme";
export const initialState = {
currentTheme: theme.dark
};
export function reducer(state, action) {
switch (action.type) {
case "setTheme":
return { ...state, currentTheme: action.value };
case "updateTheme":
return {
...state,
currentTheme: { ...theme[state.currentTheme.id], ...action.value }
};
case "toggleTheme": {
const newThemeKey = state.currentTheme.id === "dark" ? "light" : "dark";
return { ...state, currentTheme: theme[newThemeKey] };
}
default:
throw new Error();
}
}
5. Adding theme file
This theme file consist of the light and dark theme, as well as some basic styling.
const base = {
easeOutBack: "cubic-bezier(0.34, 1.56, 0.64, 1)",
colorWhite: "rgb(255, 255, 255)",
colorBlack: "rgb(0, 0, 0)"
};
const dark = {
id: "dark",
...base,
backgroundColor: "#333",
textColor: 'black',
navColor: "indianred"
};
const light = {
id: "light",
...base,
backgroundColor: "#333",
textColor: 'white',
navColor: "lightcoral"
};
export const theme = { dark, light };
6. Making it work!
First, let's make the toggle work. Inside Nav.js add an onClick handler on the toggle styled div and call our dispatch function we will retrieve with useContext():
import React, { useContext } from "react";
import styled from "styled-components";
import { AppContext } from "./index";
export default function Nav() {
const { dispatch } = useContext(AppContext);
const toggleTheme = () => {
dispatch({ type: "toggleTheme" });
};
return (
<NavBar>
<NavMenu>
<NavLink>Home</NavLink>
<NavLink>About Us</NavLink>
<NavLink>Contact</NavLink>
</NavMenu>
<NavToggle onClick={toggleTheme}>Toggle theme</NavToggle>
</NavBar>
);
}
...
To make our theme work, we have to set the colors as variables, based on the props we retrieve from our theme. Inside Nav.js:
...
const NavBar = styled.div`
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
background: ${props => props.theme.navColor};
min-height: 50px;
font-size: 1.2rem;
font-weight: 500;
color: ${props => props.theme.textColor};
list-style: none;
`;
...
Great, it should be working now! You can do the same thing with the colors inside the index.js to apply the effect everywhere in our app.
Checkout my code sandbox for the full code:
Hope you learned something from this tutorial! Make sure to follow me for more.
Top comments (1)
That's literally what I needed.