X
Popular Searches

How to Use React Contexts to Manage State

React logo on a dark background

React contexts are a feature which help you eliminate repetitive prop drilling. Props are often passed down from a parent component to a deeply nested child. This requires each intermediary component to “forward” the prop to the next component in the tree.

Contexts let you pass props through the tree without manually forwarding them at each level. This is particularly useful for data representing top-level application state. User settings, authentication tokens and data cached from API responses are good candidates for the Context API.

This data is often managed by a dedicated state store such as Redux. React’s contexts can often be used instead. This removes a significant amount of complexity within apps where Redux is only used to hold application-level state. Instead of creating an action, dispatcher and reducer, you can use a React context.

An Example Without Context

After a user logs in, you’ll often want to store essential data such as their name and email address in your app’s state. This allows each component to display information about the user without making a round-trip to your API server.

Here’s a naive way of implementing this:

const UserProfileLink = ({user}) => {
    return (
        <div>
            <p>Logged in as {user.Name}</p>
            <a href="/logout">Logout</a>
        </div>
    );
};
 
const Header = ({user}) => {
    return (
        <div>
            <h1>My App</h1>
            <UserProfileLink user={user} />
        </div>
    );
}
 
const App = () => {
    const [user, setUser] = useState({
        Name: "Foo Bar",
        Email: "foobar@example.com"
    });
    return <Header user={user} />
}

There are three components in the app. The top-level App component keeps track of the current user within its state. It renders the Header component. Header renders UserProfileLink, which displays the user’s name.

Advertisement

Crucially, only UserProfileLink actually interacts with the user object. Header still has to receive a user prop though. This is then forwarded straight into UserProfileLink, without being consumed by Header.

The problems with this approach are exasperated as your component tree becomes more complex. You might forward props through multiple nested levels, even though the intermediary components don’t use the props themselves.

Adding the Contexts API

You can mitigate these issues using the Contexts API. We can refactor the example above to remove the user prop from Header.

The App component will create a new context to store the current user object. UserProfileLink will consume the context. The component will be able to access the user data provided by the context. This is a similar concept to connecting a component to a Redux store.

const UserContext = React.createContext(null);
 
const UserProfileLink = () => {
    const user = useContext(UserContext);
    return (
        <div>
            <p>Logged in as {user.Name}</p>
            <a href="/logout">Logout</a>
        </div>
    );
};
 
const Header = () => {
    return (
        <div>
            <h1>My App</h1>
            <UserProfileLink />
        </div>
    );
}
 
const App = () => {
 
    const [user, setUser] = useState({
        Name: "Foo Bar",
        Email: "foobar@example.com"
    });
 
    return (
        <UserContext.Provider value={user}>
            <Header />
        </UserContext>
    );
 
}

This refactored set of components illustrates how to use Contexts.

The process starts by creating a new context for the value you’d like to make available. The call to React.createContext(null) sets up a new context with a default value of null.

Advertisement

The App component has been rewritten to wrap Header in UserContext.Provider. The value prop determines the current value of the context. This is set to the user object held in state. Any components nested below the context provider can now consume the context and access the user object.

If you try to access the context from a component that’s not nested within a provider instance, the component will receive the default value you passed to createContext(). This should generally be avoided except for static context values which will never change.

Access to the provided context value is observed in UserProfileLink. The user prop has been removed. The component uses React’s useContext() hook to retrieve the current value of the UserContext. This will provide the user object injected by the App component!

The final change is to the Header component. This no longer needs to forward the user prop so it can be removed entirely. In fact, the user prop is gone from the entire app. It’s now fed by App into the UserContext provider, not any specific component.

Using Context With Class Components

So far we’ve only used contexts within functional components. Contexts work well here as the useContext() hook simplifies accessing the provided data within child components.

You can also use contexts with class components. The preferred way is to set the static contextType class property to the context instance you want to use. React will read this property and set the context property on component instances to the current value provided by the context.

const UserContext = React.createContext({Name: "Foo Bar"});
 
class MyComponent extends React.Component {
 
    // Tell React to inject the `UserContext` value
    static contextType = UserContext;
 
    render() {
        // Requested context value made available as `this.context`
        return <p>{this.context.Name}</p>;
    }
 
}
Advertisement

An alternative approach is to render your component’s children within a context “consumer”. You can access each context’s consumer as the Consumer property of the context instance.

You must provide a function as the consumer’s child. The function will be called with the context’s value when the component renders.

Here’s what this looks like:

const UserContext = React.createContext({Name: "Foo Bar"});
 
class MyComponent extends React.Component {
 
    render() {
        return (
            <UserContext.Consumer>
                {user => <p>{user.Name}</p>}
            </UserContext.Consumer>
        );
    }
 
}

Using contextType restricts you to one context per component. Context consumers address this issue but can make your component’s render method more opaque. Neither approach is quite as straightforward as the useContext() hook available to functional components.

Updating Context Values

Context values function similarly to props. If a child needs to update context values, add a function to the context. The child could call the function to effect the context value change.

const UserContext = React.createContext(null);
 
const UserProfileLink = () => {
    const context = useContext(UserContext);
    return (
        <div>
            <p>Logged in as {context.user.Name}</p>
            <a onClick={() => context.logoutUser()}>Logout</a>
        </div>
    );
};
 
const Header = () => {
    return (
        <div>
            <h1>My App</h1>
            <UserProfileLink />
        </div>
    );
}
 
const App = () => {
 
    const [user, setUser] = useState({
        Name: "Foo Bar",
        Email: "foobar@example.com"
    });
 
    const contextValue = {
        user,
        logoutUser: () => setUser(null)
    }
 
    return (
        <UserContext.Provider value={contextValue}>
            <Header />
        </UserContext>
    );
 
}

This revised example shows how App now provides a logoutUser function within the context. Context consumers can call this function to update the user in the App component’s state, causing the context’s value to be modified accordingly.

Managing Re-Renders

React will re-render all children of a context provider whenever the provider’s value prop changes. Value changes are compared using Object.is(), which means you must take care when using objects as the context provider’s value.

const App = () => {
    return (
        <ExampleContext.Provider value={{foo: "bar"}}>
            <NestedComponent />
        </ExampleContext.Provider>
    );
}
Advertisement

This component would re-render NestedComponent every time App renders. A new object instance is created for the provider’s value prop each time, so React re-renders the component’s children.

You can address this by lifting the value object into the component’s state. This will ensure the same object is rendered each time:

const App = () => {
    const [obj, setObj] = useState({foo: "bar"});
    return (
        <ExampleContext.Provider value={obj}>
            <NestedComponent />
        </ExampleContext.Provider>
    );
}

Summary

React Contexts help you cut out prop drilling by providing a first-class way to pass data down the component tree. Contexts are a good alternative to state libraries like Redux. They’re built-in to React and are gaining traction among projects where Redux is used solely to connect deeply nested components to a shared source of state.

Contexts are intended for “global” data held within your application. They may also hold data for a specific sub-section of your app. Contexts aren’t meant to replace local component state entirely. Individual components should continue to use state and props where the data’s only going to pass up and down a shallowly nested tree.

James Walker James Walker
James Walker is a contributor to CloudSavvy IT. He is the founder of Heron Web, a UK-based digital agency providing bespoke software development services to SMEs. He has experience managing complete end-to-end web development workflows, using technologies including Linux, GitLab, Docker, and Kubernetes. Read Full Bio »

The above article may contain affiliate links, which help support CloudSavvy IT.