Categories
React

Understanding the React Context API & useContext Hook

Passing data (i.e. props) between components has always been one of those things with React that’s just a little… annoying.

Once again, though, the React team have provided an alternative approach that makes this a breeze. That solution is Context.

What is the Context API, and what problems does it solve?

React’s Context API provides a set of methods that help us get data from one component to another without having to pass it up or down via props.

Typically, in React we have to “lift state up” in order for it to be accessible to other components elsewhere in our application. The same goes with regular data you want to pass along. This can be tiresome and is especially so in larger apps.

Context solves that problem by allowing you to bypass all of the components in the chain that don’t need that data, sending it instead directly to the target component. In a way, it works a bit like the usePortal hook (which I wrote about here) in that it sends data from here directly to there.

How does Context work?

React Context works in three parts; by first creating the context, then providing it, then the component on the other end consumes it.

1. Creating the Context

Your context can live in separate files in a new directory within your /src folder. Best practice is to name it something like /store.

Within that new directory, create a regular Javascript file which will house your context.

In that file, call React.createContext() to create a special object that contains components and store it in a variable. The argument you provide to that method is your context data, so it could be a string, an object, a function or anything else.

Remember to export it and voilá!

import React from 'react';

const AuthContext = React.createContext({
    isLoggedIn: false
})

export default AuthContext;

Handy Tip: If you’re using VS Code, you can ensure it autocompletes your context functions by providing a dummy function like so; onLogout: () => {}.

You can take your context even further and include a lot of your logic in there, too, which leaves your render-focused component to focus mainly on presentation.

import React, { useState, useEffect } from "react";

const AuthContext = React.createContext({
  isLoggedIn: false,
  onLogout: () => {},
  onLogin: (email, password) => {},
});

export const AuthContextProvider = (props) => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  useEffect(() => {
    const storedUserLoggedInInformation = localStorage.getItem("isLoggedIn");

    if (storedUserLoggedInInformation === "1") {
      setIsLoggedIn(true);
    }
  }, []);

  const logoutHandler = () => {
    localStorage.removeItem("isLoggedIn");
    setIsLoggedIn(false);
  };

  const loginHandler = () => {
    localStorage.setItem("isLoggedIn", "1");
    setIsLoggedIn(true);
  };

  return (
    <AuthContext.Provider
      value={{
        isLoggedIn: isLoggedIn,
        onLogout: logoutHandler,
        onLogin: loginHandler,
      }}
    >
      {props.children}
    </AuthContext.Provider>
  );
};

export default AuthContext;

2. Providing the Context

Providing the context is essentially just making it available to a given component or set of components.

To do this, wrap your target component(s) in <YourContextComponent.Provider>. It’s now available to all of the children component within that.

...
  return (
    <AuthContext.Provider>
      <MainHeader isAuthenticated={isLoggedIn} onLogout={logoutHandler} />
      <main>
        {!isLoggedIn && <Login onLogin={loginHandler} />}
        {isLoggedIn && <Home onLogout={logoutHandler} />}
      </main>
    </AuthContext.Provider>
  );
...

Handy Tip: The Provider is just a component, so you can remove any containing <div>, <> or <Fragment> that you might’ve been had and not needed.

3. Consuming Context with useContext Hook

Finally, we consume the context. We can do this in two ways; one with <AuthContext.Consumer> and the other with the useContext() hook. Here, we’ll focus on the latter, more elegant hook approach to consuming context.

All we have to do is:

  1. Import the context component.
  2. Use the hook, such as const ctx = useContext(AuthContext).
  3. Reference the context variable (ctx) as if it were props wherever you need the data.
...
const Navigation = (props) => {
  const ctx = useContext(AuthContext);
  return (
    <nav className={classes.nav}>
      <ul>
        {ctx.isLoggedIn && (
          <li>
            <a href="/">Users</a>
          </li>
        )}
...

Limitations of Context

While context seems like it may be a bit of a no-brainer when it comes to passing data around your application with ease, there are some things to consider:

  • Context isn’t great for re-usable components as you’re essentially hard-coding specific props instead of keeping them more abstract.
  • Context isn’t optimised for high-frequency changes.

Ultimately, try to avoid using it for every situation where you need to pass props around. Instead, use it only when you have long prop chains.

Example of useContext usage

Implementing a Dark Mode Toggle

In this example, we’re going to implement an dark mode toggle that will affect multiple components, so we can go from light to dark. You’d typically want this to affect the entire app.

First, we’ll create our context which includes the necessary toggleTheme method that will toggle our theme state between “light” and “dark”.

import React, { useState } from "react";

const ThemeContext = React.createContext(null);

export const ThemeContextProvider = (props) => {
  const [theme, setTheme] = useState("light");

  const toggleTheme = (event) => {
    event.preventDefault();
    setTheme(theme === "light" ? "dark" : "light");
  };

  return (
    <ThemeContext.Provider value={{ theme: theme, toggleTheme: toggleTheme }}>
      {props.children}
    </ThemeContext.Provider>
  );
};

export default ThemeContext;

Wrap our <App> component in the ThemeContextProvider component we created.

import ...
import { ThemeContextProvider } from "./store/theme-context";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <ThemeContextProvider>
      <App />
    </ThemeContextProvider>
  </React.StrictMode>
);

We can skip providing the context to the <App> component and the parent component of the children we’re working with in this example.

Here, we import the context component, utilise the useContext() hook to consume the context that was provided in index.js via our custom provider. We can then access our state using ctx.theme.

import React from "react";
import ThemeContext from "../../store/theme-context";
import ContextExampleToggleButton from "./ContextExampleToggleButton";

const ContextExampleLeft = (props) => {
  const ctx = React.useContext(ThemeContext);

  return (
    <div
      style={{
        width: "50%",
        display: "inline-block",
        boxSizing: "border-box",
        padding: "2rem",
        background: ctx.theme === "light" ? "#EFEFEF" : "#222222",
        color: ctx.theme === "light" ? "#222222" : "#EFEFEF",
      }}
    >
      <p>Left</p>
      <ContextExampleToggleButton />
    </div>
  );
};

export default ContextExampleLeft;

And finally, for the toggle button, we do the same as above and can provide ctx.toggleTheme to the onClick listener.

import React from "react";
import ThemeContext from "../../store/theme-context";

const ContextExampleToggleButton = (props) => {
  const ctx = React.useContext(ThemeContext);

  return (
    <button
      onClick={ctx.toggleTheme}
      style={{
        border: "none",
        color: ctx.theme === "light" ? "#EFEFEF" : "#222222",
        background: ctx.theme === "light" ? "#222222" : "#EFEFEF",
        borderRadius: "0.5rem",
        padding: "1rem",
        cursor: "pointer",
      }}
    >
      Toggle Theme
    </button>
  );
};

export default ContextExampleToggleButton;

Leave a Reply

Your email address will not be published. Required fields are marked *