Recently I have been reading some stuff about some features included in React that I have been missing out, by scrolling on the documentation I noticed that there is a built in tool that I was always trying to get my hands on, the Context API!.
As far as I understood reading the documentation I consider the React Context API as tool designed to store data that we can consider global and needs to be accessible for all or the majority of the components in our app. So I thought on building a simple app based on the react documentation to see how context api works and sharing what I learned.
What are goint to build?
We are going to build a simple app with a dynamic theme based on react documentation to showcase the basics of the context api.
Project structure:
css file: app.css
Creating the context
The first step is to create the conext (global values) that the app will use, we create our context object using the react function createContext() which will return a object with two special properties Provider and Consumer.
import {createContext} from "react";
const context = createContext('defaultValue');
The Provider its the property we will use as a parent element to provide (forgive the redundancy) the golbal state to all the children that use the Consumer property so they can have access to values inside the context.
Note that we pass a parameter to the createContext function, this parameter work as a default value of the context but it will be only used when a component does not have a Provider above of it in the element tree. But that does not mean the default value won't be useful, it can be used to set the structure of the context so it can be checked for reference, and to test componets in an isolated way without having to wrap them in a provider.
So, now we can make the context that our app will use.
import {createContext} from "react";
//simple object to contain the supported themes on the app
export const themes = { light: "light", dark: "dark" };
export const ThemeContext = createContext({
theme: themes.light,
toggleTheme: () => {}
});
We create a theme object to contain the themes the app will use, and we create our ThemeContext that will provide us with the context. As mentionted earlier, we pass a default value as a parameter to createContex, in this case we are passing a object with the property theme which with the light theme set and a toggleTheme property that has a function, even tho this value wont be used by the children consuming the context, its nice to have a reference of how the context should be structured.
Note: the Provider and Consumer usage in this example works for both class based and hook based components.
Exposing the context
We have our context done, now we have to expose it so all the child components can have access to it.
import { useState } from "react";
import { ThemeContext, themes } from "./context/context";
import Home from "./Home";
export default function App() {
//if you are using classes then this will be your render function
return (
<ThemeContext.Provider value={{ theme: theme.light}}>
//we will build this component next, don't worry
<Home />
</ThemeContext.Provider>
);
}
In our app.js we expose the context to all of the children by wrapping them in the Provider property of out ThemeContext, the prop value is the actual data that will be accessible to all of the context consuming components. The context available will have a property called Theme with the value set to "light".
Now we are going to proceed creating the first component that will consume our context.
import React from "react";
import { ThemeContext } from "./context/context";
import "./assets/app.css";
const Home = () => (
<ThemeContext.Consumer>
{({ theme }) => (
<label>Using {theme} theme </label>
)}
</ThemeContext.Consumer>
);
export default Home;
In our Home component we make use of the Cosumer property of ThemeContext to access the context. Inside the consumer we define a function that receives as parameter the state we defined in our Provider value in app.js and returns the view elements. By deconstructing it we obtain the property theme and then displaying it in a label.
Now all the components can access to the context by wrapping them inside the Consumer and using the data they need. But as right now there is one little detail, the context can not be changed! we can not change the default theme to the dark theme, and that is our next step.
Updating the context
We want to be able to change the theme our app uses, to make it possible we are going to make some adjustments to our app.js and create a new component to manage the theme change.
import { useState } from "react";
import { ThemeContext, themes } from "./context/context";
import Home from "./Home";
export default function App() {
//if your are using classes adapt this to the component state object
const [currentTheme, setCurrentTheme] = useState(themes.light);
const toggleTheme = () =>
setCurrentTheme((theme) =>
theme === themes.light ? themes.dark : themes.light
);
//if your are using classes this will be your render function
return (
<ThemeContext.Provider value={{ theme: currentTheme, toggleTheme }}>
<Home />
</ThemeContext.Provider>
);
Using the useState hook (or state object if you are using classes) we now have state that will keep the value of the current theme applied and define the toggleThem function to change currentTheme value, then we pass them in the value prop of the Provider so the children components now can have access to the toggleTheme function to change and update the currentTheme value hence updating the context.
To avoid repeating code we now create a toggle theme button component which function will be to change the app theme.
import { ThemeContext } from "../context/context";
const ThemeTogglerButton = () => (
<ThemeContext.Consumer>
{({ toggleTheme }) => <button onClick={toggleTheme}>Change Theme</button>}
</ThemeContext.Consumer>
);
export default ThemeTogglerButton;
We now can use the component in any other part of out app while it's inside the Provider, in this example toggle button is going to be used on our Home view, also were are going to apply some changes to the Home layout for a better look.
import React from "react";
import { ThemeContext } from "./context/context";
import "./assets/app.css";
const Home = () => (
<ThemeContext.Consumer>
{({ theme }) => (
<div className={`home ${theme}`}>
<div className="header">
<h3>Using {theme} theme</h3>
</div>
<div className="content">
<ThemeTogglerButton />
</div>
</div>
)}
</ThemeContext.Consumer>
);
export default Home;
Home imports the ThemeTogglerButton to update the app theme, also we added two new sections, one to work as header to display the current theme in use and the other as a main content where the toggleButton is placed.
useContext hook
React provides a hook called useContext which allow us to have access to the context and use it in our component structure not only on the return value using the Consumer. To give a better example there is a component that implements the useContext hook and depending on the current theme changes the description it shows.
import React, { useContext, useEffect, useState } from "react";
import { ThemeContext, themes } from "../context/context";
const ThemePanel = () => {
//accesing the theme property from the context
const { theme } = useContext(ThemeContext);
const [icon, setIcon] = useState("");
useEffect(() => {
setIcon(String.fromCodePoint(theme === themes.light ? 0x1f31e : 0x1f319));
}, [theme]);
return (
<div className="panel">
A {theme} {icon} theme with react context API{" "}
<span id="atom-icon">⚛</span>
</div>
);
};
export default ThemePanel;
Impleteming the ThemePanel component in Home.
import React from "react";
import { ThemeContext } from "./context/context";
import "./assets/app.css";
const Home = () => (
<ThemeContext.Consumer>
{({ theme }) => (
<div className={`home ${theme}`}>
<div className="header">
<h3>Using {theme} theme</h3>
</div>
<div className="content">
<ThemePanel />
<ThemeTogglerButton />
</div>
</div>
)}
</ThemeContext.Consumer>
);
export default Home;
The useContext hook will return the context value and by deconstructing it we can access to theme property that is the one the component needs. We also define a icon state to hold the value of and icon (in this case an emoji) and the useEffectHook to update the icon value depending on the applying theme. With this hook the context state can be used on the components logic and on the return value. If you're using classes the useEffect hook can be adapted by using componentDidMount and componentDidUpdate functions.
Note: as with the Consumer the context can only be accessed if the component using this hook is inside a Provider
So, should I use Consumer or useContext if my app is hook based?
Yes.
But seriously, if your app is hook based go with useContext and if is class based use the Consumer, but as you saw in the examples Consumer works also fine with hooks and as in the ThemeTogglerButton component if its a pure component the consumer can help you keeping the code straightforward.
Now you have a simple app built using the context api! you can keep testing things out by adding more elements to the context and see where this api can be useful to you!
Before you try
We have seen how Context API can help us defined values to be used across the app and how it can be useful reducing the prop drilling problem, but there are some details to keep in mind if your going to implement this:
Context API will cause a global re-render: If you update the context of the application like in this example, keep in mind this change will cause a re-render on all components that are implementing a consumer (unlike redux where it only re-render the components that have been updated).
Even though we can say this can be managed by using React.memo,useCallback or useMemo, we will be adding complexity in places where is not needed.
Don't use context as a state management tool: As we now a state management tool give us ways to store, read and update a value. Context API give us a way to access a value on all components where we would need it, but in the example we saw that the actual state management was done by useState on the App component and a function to update it.
Avoid constant mutation: Based on the other points mentioned before, try to avoid mutations of the context on a regular basis. Context updates are more concise if the changes will be applied for all components, like in this example where the theme change will update the global appearance of the app.
Context API is more suitable for data that won't be changed frequently, like the selected theme, settings, user info, etc.
Full example can be found here:
Simple Context API APP
Repository:
React Context Api Example
Further reading: React Context API