Moving from in-component state to using React Context

React
useContext

Contents

When you start out with React, state is one of the first things that you learn about. You quickly become familiar with the useState hook and learn how to manage state at a component level. And life is good ... for a while.

As you become more proficient with React, and your applications grow in complexity, it's only a matter of time before it becomes difficult to manage state at the component level. At some point you will find that you need to pass state through a few levels of components, usually because a top-level component needs access to a particular piece of state that is modified by a component that's two or three levels deep. You end up passing props through components that never actually use them simply to make them available in a 'destination' component.

This practice is commonly referred to as prop drilling

React version 16 introduced Context Hooks which the React docs refer to as a means of letting "a component receive information from distant parents without passing it as props." In other words it's a way handling state at a global level and avoiding the complications of prop drilling.

Don't assume that using Context Hooks automatically means that state is managed globally. That doesn't have to be the case: the Context in question might wrap only specific components.

I will continue to refer to it as 'global' just for the sake of simplicity.

in-component state

global context state

The diagrams seeks to explain how React makes state available using the two different approaches: in-component useState Hooks and 'global' Context Hooks. The components named in the diagrams feature in an example we will look at shortly. You will see that it's not a complex example. It demonstrates multi-layered prop drilling to a small extent. I want to keep things simple and focus on the mechanism.

Having said that I hope that, even with such a small application, you get an insight into the 'inter-connectedness' between components where props have to be passed compared to the simpler, less structured situation where a 'global' Context Hook is used.

React makes it possible for us to give multiple components access to state that is managed at a global level through:

  • a Context Provider (part of the Context object that we will look at in more detail soon), and
  • its children prop

The Context Provider is, in many ways, a regular React functional component that takes a children prop. Within the component's function body we can define certain values:

  • pieces of state, and
  • functions that affect those pieces of state

Those values are automatically made avilable to components that are children of the Context Provider.

Going back to the diagram above, our Context Provider takes on the role of the pale red wrapper.

Now is probably a good time to look at an example.

I introduced React Context as a way to avoid prop drilling. In an ideal world I guess we could set up a complex multi-layered application with lots of prop drilling to demonstrate how we might refactor the app to use Context Hooks to simplify state handling. But that would be too complex for an article like this.

The small example that follows focuses on a part of state that many components commonly need access to: users. You will need to use your imagination to think of ways that the prnciples shown here could have wider applications.

This small application has just three components: App.jsx, Users.jsx and UserItem.jsx.

// App.jsx import { useState } from 'react'; import Users from './Users'; function App() { const [users, setUsers] = useState([]); return ( <div> <Users users={users} setUsers={setUsers} /> </div> ); } export default App;
// Users.jsx import { useEffect, useState } from 'react'; import UserItem from './UserItem'; function Users({ users, setUsers }) { const [isLoading, setIsLoading] = useState(false); useEffect(() => { fetchUsers(); }, []); const fetchUsers = async () => { setIsLoading(true); const response = await fetch('https://jsonplaceholder.typicode.com/users'); const data = await response.json(); setUsers(data.slice(0, 5)); // restrict to 5 setIsLoading(false); }; return ( <div> {isLoading ? ( <p>Loading...</p> ) : ( users.map((user) => <UserItem key={user.id} user={user} />) )} </div> ); } export default Users;
// UserItem.jsx function UserItem({ user }) { return ( <div> <p> Name: <strong>{user.name}</strong> </p> <p>Email: {user.email}</p> <p>Website: {user.website}</p> <hr /> </div> ); } export default UserItem;

Functionality

The app:

  • fetches a list of users from the jsonplaceholder dummy data API, and
  • renders (some of) their details

Two elements of state (users and isLoading) are handled with a useState Hook and a fetchUsers function is triggered by a useEffect on first render.

Users state is held in App.jsx because Users data is required by other components that can't be seen in this example and Users state will need to be passed down.

The rendered output

example output

  • Create a context directory inside src

    This is not obligatory - context can live anywhere - but it makes sense to group context components together

  • In there, create a Users Context file called UsersContext.jsx. Note the .jsx extension. The contents of this file are put together the same as a regular React component and it returns jsx.

Inside our Users Context file

Call the React createContext() function (this must be imported from 'react') and assign its return value to a UsersContext variable.

UsersContext now contains a context object giving us access to a context Provider and a context Consumer. We will need access to UsersContext from other modules so export it as default from this module.

Now create, and export, a UsersProvider functional component:

  • it takes a single prop: children
  • copy into it the useState hooks from App and Users
    • both users and isLoading
    • remember to import useState from 'react'
  • also copy into it the fetchUsers function
    • in order to do this, fetchUsers() must be defined outside the useEffect hook and called from within it. The useEffect is, incidentally, staying in the Users component.

      As an aside, it's ok to define async functions outside useEffect hooks simply so you can use async/await syntax.

  • return a special type of component which makes use of the Provider property of UsersContext
    • called <UsersContext.Provider>
  • pass to the Provider a prop called value which is an object with properties:
    • users
    • isLoading
    • fetchUsers
  • and the Provider component should wrap children, like this:
    • <UsersContext.Provider value={{ users, isLoading, fetchUsers, }} > {children} </UsersContext.Provider>
    • This structure makes available (ie. Provides) all of the value object's properties to any component wrapped inside UsersContext.Provider that wants to Consume them.

Those steps above will make more sense if you look at the finised article:

// context/UsersContext.jsx import { createContext, useState } from 'react'; const UsersContext = createContext(); export const UsersProvider = ({ children }) => { const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(false); const fetchUsers = async () => { setIsLoading(true); const response = await fetch('https://jsonplaceholder.typicode.com/users'); const data = await response.json(); setUsers(data.slice(0, 5)); // restrict to 5 setIsLoading(false); }; return ( <UsersContext.Provider value={{ users, isLoading, fetchUsers, }} > {children} </UsersContext.Provider> ); }; export default UsersContext;

In App.jsx

  • delete the useState hook and the useState import
  • we don't need to pass any props to Users
  • wrap Users in <UsersProvider> which needs to be imported
import Users from './Users'; import { UsersProvider } from './context/UsersContext'; function App() { return ( <UsersProvider> <Users /> </UsersProvider> ); } export default App;

In Users.jsx

  • remove props from the function parentheses
  • delete the useState hook and useState import
  • delete the fetchUsers function
  • destructure users, isLoading and fetchUsers from useContext(UsersContext)
    • import useContext from 'react'
    • also import our UsersContext
import { useContext, useEffect } from 'react'; import UserItem from './UserItem'; import UsersContext from './context/UsersContext'; function Users() { const { users, isLoading, fetchUsers } = useContext(UsersContext); useEffect(() => { fetchUsers(); }, []); return ( <div> {isLoading ? ( <p>Loading...</p> ) : ( users.map((user) => <UserItem key={user.id} user={user} />) )} </div> ); } export default Users;

UserItem.jsx

This is unaffected. It still receives the user prop from Users.

Other components

Any other components that need access to users state can be wrapped in the <UsersProvider> component and destructure users from useContext.

We have refactored our small application to make use of a Context Hook to handle state globally rather than inside individual components. The app functions exactly the same as it did before the refactoring.

The main effect is that we no longer need to pass props around simply to make a particular piece of state available to a particular component.

React Context doesn't have to operate at a global level. In many cases we will want to use a Context Provider to wrap only a specific selection of components that need access to one or more specific state variables.

A further improvement that we could make to our UsersContext file would be to use Reducers to manage state rather than useState hooks. That will be the subject of another article.