Context API is a great feature offered by React, but it can be tricky to get it right. Learn how to efficiently create and consume Context API with the use of React Hooks without performance issues. Starting with a naive implementation, we will iterate over what can be improved and how to avoid unnecessary component re-renders.
Since version 16.3, React has had a stable version of Context API that can be used to easily share data between many components. It can be passed down directly to components that need it while avoiding prop drilling. In this article you will learn how to use Context efficiently without introducing performance bottlenecks.
Imagine you have an application that has a global spinner which shows an overlay that covers the whole page while an app is communicating with a server. A function to show and hide a spinner should be accessible from any component in the application.
Let’s start with a simple implementation and then we will iterate through how it can be improved. First, create a new project with create-react-app
. If you don’t know, it is a CLI tool for scaffolding React projects. Make sure you have Node.js installed on your machine. If you have any issues with creating a project then check the official site - https://create-react-app.dev/.
npx create-react-app context-app
When the project is ready, we have to create a few files.
src/context/GlobalSpinnerContext.js
src/components/GlobalSpinner/GlobalSpinner.js
src/components/GlobalSpinner/globalSpinner.css
src/components/RandomComments.js
In the GlobalSpinnerContext.js file we will create our Context logic and GlobalSpinnerContext provider, while the GlobalSpinner folder will have the Spinner component and styles. RandomComments.js file will fetch comments from an API and will trigger GlobalSpinner when needed.
src/components/RandomComments.js
The RandomComments component will render a list of comments. When it is mounted, it will make an API call to get comments and then use setComments
to update the state and display them.
import React, {useState, useEffect} from 'react'
const RandomComments = props => {
const [comments, setComments] = useState([])
useEffect(() => {
(async () => {
const result = await fetch('https://jsonplaceholder.typicode.com/comments')
const data = await result.json()
setComments(data)
})()
}, [])
return (
<div>
{comments.map(comment => {
const {name, body, id} = comment
return (
<div key={id}>
<p style={{fontWeight: 'bold'}}>{name}</p>
<p> {body}</p>
</div>
)
})}
</div>
)
}
export default RandomComments
src/components/GlobalSpinner/GlobalSpinner.js
Simple component which has an overlay and Loading
text. You can be fancier if you like.
import React from 'react'
import './globalSpinner.css'
const GlobalSpinner = props => {
return (
<div className="global-spinner-overlay">
<p>Loading...</p>
</div>
)
}
export default GlobalSpinner
src/components/GlobalSpinner/globalSpinner.css
Styling for the overlay and loading text.
.global-spinner-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
font-size: 30px;
color: white;
font-weight: bold;
display: flex;
justify-content: center;
align-items: center;
}
src/App.js
Imports and renders GlobalSpinner and RandomComments.
import React from 'react';
import './App.css';
import GlobalSpinner from './components/GlobalSpinner/GlobalSpinner'
import RandomComments from './components/RandomComments'
function App() {
return (
<div className="App">
<GlobalSpinner />
<RandomComments />
</div>
);
}
export default App;
If you run your project with the npm run start
command, you should see a gray background with Loading
text in the middle. We will not be getting fancy with beautiful-looking spinners, as what we currently have should be enough to go through the Context implementation.
After creating necessary files and updating App.js file, head to the GlobalSpinnerContext.js file.
import React, {createContext} from ‘react’
const GlobalSpinnerContext = createContext()
export default GlobalSpinnerContext
This is the simplest implementation where we create a context and then export it. This context could be imported and used in App.js as shown in the picture below:
App.js
import React from 'react';
import './App.css';
import GlobalSpinner from './components/GlobalSpinner/GlobalSpinner'
import GlobalSpinnerContext from './context/GlobalSpinnerContext';
import RandomComments from './components/RandomComments'
function App() {
return (
<GlobalSpinnerContext.Provider>
<div className="App">
<GlobalSpinner />
<RandomComments />
</div>
</GlobalSpinnerContext.Provider>
);
}
export default App;
However, we would have to write stateful logic for the spinner in App.js as well. Instead, let’s create a ContextProvider component which will encapsulate this logic and keep the App.js file clean.
In GlobalSpinnerContext.js
we are going to create a GlobalSpinnerContextProvider
component. Note that the GlobalSpinnerContext
constant is not a default export anymore. The ContextProvider will use useState
hook to store and update visibility state for the spinner. The first attempt for a working solution could look like this:
import React, { useState, createContext } from 'react'
export const GlobalSpinnerContext = createContext()
const GlobalSpinnerContextProvider = (props) => {
const [isGlobalSpinnerOn, setGlobalSpinner] = useState(false)
return (
<GlobalSpinnerContext.Provider value={{isGlobalSpinnerOn, setGlobalSpinner}}>
{props.children}
</GlobalSpinnerContext.Provider>
)
}
export default GlobalSpinnerContextProvider
Don’t forget to update App.js file as we use Context.Provider inside of the GlobalSpinnerContext.js file.
App.js
import React from 'react';
import './App.css';
import GlobalSpinner from './components/GlobalSpinner/GlobalSpinner'
import GlobalSpinnerContextProvider from './context/GlobalSpinnerContext';
import RandomComments from './components/RandomComments'
function App() {
return (
<GlobalSpinnerContextProvider>
<div className="App">
<GlobalSpinner />
<RandomComments />
</div>
</GlobalSpinnerContextProvider>
);
}
export default App;
Then in the GlobalSpinner
component we can import the GlobalSpinnerContext
and use it with useContext
hook.
GlobalSpinner.js
import React, {useContext} from 'react'
import './globalSpinner.css'
import {GlobalSpinnerContext} from '../../context/GlobalSpinnerContext'
const GlobalSpinner = props => {
const {isGlobalSpinnerOn} = useContext(GlobalSpinnerContext)
return isGlobalSpinnerOn ? (
<div className="global-spinner-overlay">
<p>Loading...</p>
</div>
) : null
}
export default GlobalSpinner
If you check the website, you will see that the overlay with the spinner has disappeared. This is because we set the spinner value to be false
by default. In the same manner, we can import and use the GlobalSpinnerContext
in the RandomComments
component. However, this time we don’t need the isGlobalSpinnerOn
value, but instead we need access to the setGlobalSpinner
function.
RandomComments.js
import React, {useState, useEffect, useContext} from 'react'
import {GlobalSpinnerContext} from '../context/GlobalSpinnerContext'
const RandomComments = props => {
const [comments, setComments] = useState([])
const {setGlobalSpinner} = useContext(GlobalSpinnerContext)
useEffect(() => {
(async () => {
setGlobalSpinner(true)
const result = await fetch('https://jsonplaceholder.typicode.com/comments')
const data = await result.json()
setComments(data)
setGlobalSpinner(false)
})()
}, [setGlobalSpinner])
return (
<div>
{comments.map(comment => {
const {name, body, id} = comment
return (
<div key={id}>
<p style={{fontWeight: 'bold'}}>{name}</p>
<p> {body}</p>
</div>
)
})}
</div>
)
}
export default RandomComments
This is a very simple implementation which works for this scenario, but there are problems with it.
First issue is about how we are passing isGlobalSpinnerOn
and setGlobalSpinner
to the Provider.
<GlobalSpinnerContext.Provider value={{isGlobalSpinnerOn, setGlobalSpinner}}>
{props.children}
</GlobalSpinnerContext.Provider>
All context consumers are re-rendered whenever a value passed to the Provider
changes. This means that if we change visibility of the spinner or a parent component re-renders, both GlobalSpinner and RandomComments components will re-render. This is because we are creating a new inline object for the Provider value. One way to fix this is to use useMemo
hook which would memoize the value object. It would only be re-created when isGlobalSpinnerOn
value changes.
import React, { useState, createContext, useMemo } from 'react'
export const GlobalSpinnerContext = createContext()
const GlobalSpinnerContextProvider = (props) => {
const [isGlobalSpinnerOn, setGlobalSpinner] = useState(false)
const value = useMemo(() => ({
isGlobalSpinnerOn,
setGlobalSpinner
}), [isGlobalSpinnerOn])
return (
<GlobalSpinnerContext.Provider value={value}>
{props.children}
</GlobalSpinnerContext.Provider>
)
}
export default GlobalSpinnerContextProvider
This fixes the issue of re-creating a new object on every render and thus re-rendering all consumers. Unfortunately, we still have a problem.
As we have it now, a new value object will be created whenever spinner visibility changes. However, while the GlobalSpinner component relies on the isGlobalSpinnerOn
, it does not rely on the setGlobalSpinner
function. Likewise, RandomComments requires access to the setGlobalSpinner
function only. Therefore, it does not make sense to have RandomComments re-render every time the spinner visibility changes, as the component does not directly depend on it. Therefore, to avoid this issue we can create another context to separate isGlobalSpinnerOn
and setGlobalSpinner
.
import React, { useState, createContext } from 'react'
export const GlobalSpinnerContext = createContext()
export const GlobalSpinnerActionsContext = createContext()
const GlobalSpinnerContextProvider = (props) => {
const [isGlobalSpinnerOn, setGlobalSpinner] = useState(false)
return (
<GlobalSpinnerContext.Provider value={isGlobalSpinnerOn}>
<GlobalSpinnerActionsContext.Provider value={setGlobalSpinner}>
{props.children}
</GlobalSpinnerActionsContext.Provider>
</GlobalSpinnerContext.Provider>
)
}
export default GlobalSpinnerContextProvider
Thanks to having two context providers components can consume exactly what they need. Now, we need to update GlobalSpinner
and RandomComments
components to consume correct values.
GlobalSpinner.js
The only change is that we don’t destructure isGlobalSpinnerOn
anymore.
import React, {useContext} from 'react'
import './globalSpinner.css'
import {GlobalSpinnerContext} from '../../context/GlobalSpinnerContext'
const GlobalSpinner = props => {
const isGlobalSpinnerOn = useContext(GlobalSpinnerContext)
return isGlobalSpinnerOn ? (
<div className="global-spinner-overlay">
<p>Loading...</p>
</div>
) : null
}
export default GlobalSpinner
RandomComments.js
We import ‘GlobalSpinnerActionsContext’ instead of ‘GlobalSpinnerContext’. Also, we don’t destructure ‘setGlobalSpinner’ function anymore.
import React, {useState, useEffect, useContext} from 'react'
import {GlobalSpinnerActionsContext} from '../context/GlobalSpinnerContext'
const RandomComments = props => {
const [comments, setComments] = useState([])
const setGlobalSpinner = useContext(GlobalSpinnerActionsContext)
useEffect(() => {
(async () => {
setGlobalSpinner(true)
const result = await fetch('https://jsonplaceholder.typicode.com/comments')
const data = await result.json()
setComments(data)
setGlobalSpinner(false)
})()
}, [setGlobalSpinner])
We have successfully fixed our performance issue. However, there are still improvements that can be made. However, these are not about the performance, but the way we consume Context values.
To consume spinner context values in any component, we have to import the context directly as well as the useContext
hook. We can make it a bit less tedious by using a wrapper for the useContext
hook call. Head to the GlobalSpinnerContext.js
file. We will not be exporting Context values directly anymore, but instead custom functions to consume contexts.
GlobalSpinnerContext.js
import React, { useState, createContext, useContext } from 'react'
const GlobalSpinnerContext = createContext()
const GlobalSpinnerActionsContext = createContext()
export const useGlobalSpinnerContext = () => useContext(GlobalSpinnerContext)
export const useGlobalSpinnerActionsContext = () => useContext(GlobalSpinnerActionsContext)
const GlobalSpinnerContextProvider = (props) => {
const [isGlobalSpinnerOn, setGlobalSpinner] = useState(false)
return (
<GlobalSpinnerContext.Provider value={isGlobalSpinnerOn}>
<GlobalSpinnerActionsContext.Provider value={setGlobalSpinner}>
{props.children}
</GlobalSpinnerActionsContext.Provider>
</GlobalSpinnerContext.Provider>
)
}
export default GlobalSpinnerContextProvider
Next, we have to update GlobalSpinner
and RandomComments
and replace direct use of useContext
hook in favor of wrapper functions.
GlobalSpinner.js
import React from 'react'
import './globalSpinner.css'
import {useGlobalSpinnerContext} from '../../context/GlobalSpinnerContext'
const GlobalSpinner = props => {
const isGlobalSpinnerOn = useGlobalSpinnerContext()
return isGlobalSpinnerOn ? (
<div className="global-spinner-overlay">
<p>Loading...</p>
</div>
) : null
}
export default GlobalSpinner
RandomComments.js
import React, {useState, useEffect} from 'react'
import {useGlobalSpinnerActionsContext} from '../context/GlobalSpinnerContext'
const RandomComments = props => {
const [comments, setComments] = useState([])
const setGlobalSpinner = useGlobalSpinnerActionsContext()
useEffect(() => {
(async () => {
setGlobalSpinner(true)
const result = await fetch('https://jsonplaceholder.typicode.com/comments')
const data = await result.json()
setComments(data)
setGlobalSpinner(false)
})()
}, [setGlobalSpinner])
We don’t have to import useContext
and spinner Contexts directly anymore. Instead, we have an interface to consume these values. There is another useful improvement we can make. useContext
should only be called inside a Context.Provider
. To ensure we don’t make the mistake of using a context outside of a Provider
, we can check if there is any context value.
import React, { useState, createContext, useContext } from 'react'
const GlobalSpinnerContext = createContext()
const GlobalSpinnerActionsContext = createContext()
export const useGlobalSpinnerContext = () => {
const context = useContext(GlobalSpinnerContext)
if (context === undefined) {
throw new Error(`useGlobalSpinnerContext must be called within GlobalSpinnerContextProvider`)
}
return context
}
export const useGlobalSpinnerActionsContext = () => {
const context = useContext(GlobalSpinnerActionsContext)
if (context === undefined) {
throw new Error(`useGlobalSpinnerActionsContext must be called within GlobalSpinnerContextProvider`)
}
return context
}
As you can see on the picture above, instead of returning a result of useContext
immediately, we first check the context value. If it is undefined, an error is thrown. Nevertheless, it would be a bit repetitive to do it for every useContext
consumer function, so let’s abstract it into reusable factory function.
import React, {useState, createContext, useContext} from 'react'
const GlobalSpinnerContext = createContext()
const GlobalSpinnerActionsContext = createContext()
/* eslint-disable */
const useContextFactory = (name, context) => {
return () => {
const ctx = useContext(context)
if (ctx === undefined) {
throw new Error(`use${name}Context must be used withing a ${name}ContextProvider.`)
}
return ctx
}
}
/* eslint-enable */
export const useGlobalSpinnerContext = useContextFactory('GlobalSpinnerContext', GlobalSpinnerContext)
export const useGlobalSpinnerActionsContext = useContextFactory('GlobalSpinnerActionsContext', GlobalSpinnerActionsContext)
The useContextFactory
function accepts name
parameter which will be used in an error message and context
parameter that will be consumed. You might need to disable eslint for the useContextFactory
as it might throw an error that useContext
cannot be called inside a callback. This eslint error is thrown because the function useContextFactory
starts with the word use
, which is reserved for hooks. You can rename the function to something else like factoryUseContext
.
In this article we covered how to use and consume Context the right way while avoiding performance bottlenecks. You can find a GitHub repo for this project at https://github.com/ThomasFindlay/react-using-context-api-right-way.
Thomas Findlay is a 5-star rated mentor, full-stack developer, consultant, technical writer and the author of “React - The Road To Enterprise” and “Vue - The Road To Enterprise.” He works with many different technologies such as JavaScript, Vue, React, React Native, Node.js, Python, PHP and more. Thomas has worked with developers and teams from beginner to advanced and helped them build and scale their applications and products. Check out his Codementor page, and you can also find him on Twitter.