DEV Community

John Raptis
John Raptis

Posted on

Darkmode with React Context API(Classes and Hooks)

Darkmode is a small little feature that is pretty common these days. Now I'm not writing this article on why it's cool but I think this is a nice opportunity to learn some concepts that come with this. And it's actually the first example that I came across and made me understand how the React Context API works. First we will do this using Class components and then we will move to functional ones using Hooks.
This article was inspired by reading the docs(really?)

Prerequisites:

  • Have Node.js and npm installed globally.

  • Know the basics of React.

Source code and demo down below

  • view source (example with class components is in a different branch named classComponents)
  • view demo

What is the Context API?(Quickly)

The Context API is a way to control/handle the state of our application. A central place for all of our data.
Now you will say that 'isn't Redux for that'? And yes Redux does all of that.
You would prefer to use Context API though over something like Redux if you are dealing with a smaller application, where Redux might be a bit of an overkill.

Lets create our darkmode-app and learn as we go.

With Classes

First create you React app with the usual command.

npx create-react-app darkmode-app

Our file structure will look something like this.

assets
    |__ sun.svg
    |__ moon.svg
components
    |__ Navbar.js
    |__ MainBody.js
    |__ ToogleTheme.js
contexts
    |__ThemeContext.js

Three components in a components folder and one in a contexts folder. The later will be our single source of truth. Also we will have an assets folder for our moon and sun icons.

Some css for basic styling. I use scss so go ahead and npm install node-sass as well. Don't forget to change the extension in index.js from .css to .scss.

Our Navbar component ...

import React, { Component } from 'react';
import ToggleTheme from './ToggleTheme';

class Navbar extends Component {

    render() {
        return (
            <div className="navbar">
                <h1>Navbar</h1>
                <ToggleTheme />
            </div>
        )
    }
}

export default Navbar;

... and our MainBody component.

import React, { Component } from 'react';

class MainBody extends Component {

    render() {
        return (
            <div>
                <div className="main-body">
                    <h1>MainBody</h1>
                    <h2>Subtitle</h2>
                    <p>. . . </p>
                </div>
            </div>
        )
    }
}

export default MainBody;

Now you might have guessed it. Our state that will control in what mode we are(darkmode / lightmode) must be global and accessible from everywhere. So our changing color theme logic will live in the ThemeContext.js file.

import React, { Component, createContext } from 'react';

export const ThemeContext = createContext();

class ThemeContextProvider extends Component {
    state = {
        lightTheme: true
    }

    toggleTheme = () => {
        this.setState({ islightTheme: !this.state.lightTheme });
    }

    render() {
        const { children } = this.props;
        return (
            <ThemeContext.Provider value={{...this.state, toggleTheme: this.toggleTheme }}>
            {children}
            </ThemeContext.Provider>
        );
    }
}

export default ThemeContextProvider;

Above we imported React and createContext. createContext creates a Context object. We store that in a const named ThemeContext.

We create a component named ThemeContextProvider. This component's state will contain our global data. In this case if lightTheme is true or false.

To provide our components with the necessary data we have the Provider tag that surrounds the components that we want to pass the data to.

In our render function above we are returning our ThemeContext object we created and give it the Provider tag. We pass a value property that accepts the data we want to pass. In this case we pass an object with our state and functions(in our case toggleTheme function toggles our state).

Inside we destructure the children prop that refers to our child components. The ones we are nesting in our App.js file.

Looks like this.

import React from 'react';
import Navbar from './components/Navbar';
import MainBody from './components/MainBody';
import ThemeContextProvider from './contexts/ThemeContext';

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

export default App;

We provided our data all over our application using Provider with the ThemeContext object. Now we have to catch the data from each of our components. We do this using the Consumer tag.

In our ToggleTheme component we import the ThemeContext object.(NOT the ThemeContextProvider component) and wrap our JSX inside the render function with the ThemeContext.Consumer tag.

import React, { Component } from 'react';
import sun from '../assets/sun.svg';
import moon from '../assets/moon.svg';
import { ThemeContext } from '../contexts/ThemeContext';

class ToggleTheme extends Component {

    state = {
        icon: false
    }

    iconChange = () => {
        this.setState({ icon: !this.state.icon });
    }

    render() {
        return (
            <ThemeContext.Consumer>{(context) => {
                return (
                    <div className="toggle__box">
                        <span>
                        {this.state.icon ? (
                            <img src={moon} className="moon-icon" />                     
                        ) : (
                            <img src={sun} className="sun-icon" />
                        )}
                        </span>
                        <div className="toggle__btn" onClick={context.toggleTheme}>
                            <input type="checkbox" className="checkbox"
                            onChange={this.iconChange}  />
                            <div className="circle"></div>
                            <div className="layer"></div>
                        </div>
                    </div>
                )
            }}</ThemeContext.Consumer>
        )
    }
}

export default ToggleTheme;

Our Consumer expects a function. We pass our context and return our JSX
Note that with onClick we fire the toggleTheme function.

We also have some local state to show the proper icon based on the state of our theme.
With onChange we call the iconChange function that controls which icon should be shown.

In Navbar.js we will change the background color on darktheme. We are going to apply a className based on our lightTheme's state.

Again we import ThemeContext and apply it with the Consumer.

import React, { Component } from 'react';
import ToggleTheme from './ToggleTheme';
import { ThemeContext } from '../contexts/ThemeContext';

class Navbar extends Component {

    render() {
        return (
            <ThemeContext.Consumer>{(context) => {
            const theme = !context.lightTheme ? ' darkmode' : '';
            return (
                <div className={"navbar" + (theme)}>
                    <h1>Navbar</h1>
                    <ToggleTheme />
                </div>
                )
            }}</ThemeContext.Consumer>
        )
    }
}

export default Navbar;

We store a conditional statement in a const named theme and pass it as a className.
The same applies for our MainBody component.

import React, { Component } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';

class MainBody extends Component {

    render() {
        return (
            <ThemeContext.Consumer>{(context) => {
            const theme = !context.lightTheme ? ' darkmode' : '';
                return (
                    <div className={"" + (theme)}>
                        <div className="main-body">
                            <h1>MainBody</h1>
                            <h2>Subtitle</h2>
                            <p>. . . </p>
                        </div>
                    </div>
                )
            }}</ThemeContext.Consumer>
        )
    }
}

export default MainBody;

With Hooks

Now let's rewrite this using Hooks. I personally prefer this way since it's easier to reason about and cleaner for the eye.
Hooks provide us with special functions. There are many but we will use two.

useState()

will allow us to use state in functional components.

useContext()

will allow us to consume context in functional component.

Our Navbar component will change like this.

import React, { Component, useContext } from 'react';
import ToggleTheme from './ToggleTheme';
import { ThemeContext } from '../contexts/ThemeContext';

const Navbar = () => {
    const { lightTheme } = useContext(ThemeContext);
    const theme = !lightTheme ? ' darkmode' : '';
    return (
        <div className={"navbar" + (theme)}>
             <h1>Navbar</h1>
             <ToggleTheme />
         </div>
    )
}

export default Navbar;

We import the useContext function on top and instead of wrapping our content in a Consumer we destructure the state. (In our case the lightTheme).
And that's it.

The same will apply for MainBody.js.

import React, { Component, useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';

const MainBody = () => {
    const { lightTheme } = useContext(ThemeContext);
    const theme = !lightTheme ? ' darkmode' : '';

    return (
        <div className={"" + (theme)}>
            <div className="main-body">
                <h1>MainBody</h1>
                <h2>Subtitle</h2>
                <p>. . .</p>
            </div>
        </div>
    )
}

export default MainBody;

Going forward in our ToggleTheme component we import useContext and useState as well.
With useContext we grab the toggleTheme function and with useState we set the state of our icon.
icon is the default and with setIcon we pass the new value.(takes place in the iconChange function).

import React, { Component, useState, useContext } from 'react';
import sun from '../assets/sun.svg';
import moon from '../assets/moon.svg';
import { ThemeContext } from '../contexts/ThemeContext';

const ToggleTheme = () => {
    const { toggleTheme } = useContext(ThemeContext);
    const [icon, setIcon] = useState(true);

    const iconChange = () => {
        let newIcon = !icon;
        setIcon(newIcon);
    }

    return (
        <div className="toggle__box">
            <span>
            {icon ? (
                <img src={moon} className="moon-icon" />
            ) : (
                <img src={sun} className="sun-icon" />
            )}
            </span>
            <div className="toggle__btn" onClick={toggleTheme}>
                <input type="checkbox" className="checkbox" onChange={iconChange}  />
                <div className="circle"></div>
                <div className="layer"></div>
            </div>
         </div>
    )
}

export default ToggleTheme?

Note in our returned JSX we don't use the this keyword.

Lastly in our ThemeContext.

import React, { Component, createContext, useState } from 'react';

export const ThemeContext = createContext();

const ThemeContextProvider = (props) => {
    const [lightTheme, setLightTheme] = useState(true);

    const toggleTheme = () => {
        setLightTheme(!lightTheme);
    }
    const { children } = props;
    return (
        <ThemeContext.Provider value={{ lightTheme, toggleTheme }}>
             {children}
        </ThemeContext.Provider>
    )
}

export default ThemeContextProvider;

Again we set and change the state with useState. And again note that we don't use the this keyword.

That was it. Now you have the basic logic down. So get to work and try things of your own. That is the best way to learn.

The sooner you hit a wall the better. Trust me.

Latest comments (0)