How to Properly Use the Context API in React With Hooks

How to Properly Use the Context API in React With Hooks

Learn how to create shared state using the context API
Ferenc Almasi β€’ 2023 January 20 β€’ πŸ“– 9 min read

This lesson is a preview from our interactive course

There are a couple more hooks we need to cover as part of this course, and one of them was created based on an entire API. The context API lets usΒ read a context that is shared between components.

It is often used when a prop is needed in many places. The context API lets us avoid passing props around to simplify our codebase. There could also be cases where data is needed globally, such as user data or translations. The context API is well-suited for this task.

Looking to improve your skills? Check out our interactive course to master React from start to finish.
Master React

How to Use the Context API

To use the context API, we need to use the useContext hook. However, to use the hook, we need to create a context first. To create a new context in React, we can use the createContext call:

import { createContext } from 'react'

const UserContext = createContext({});
const UserProvider = UserContext.Provider;

export { UserContext, UserProvider };
context.js Creating a new context using createContext
Copied to clipboard!

Create a new file called context.js in the src directory and add the above code. This file creates a new context with a default value of an empty object. Notice that the createContext call also adds a ProviderΒ to our variable. We need to export both to start using the useContext hook.

To expose the context to our application, we need to wrap it with our UserProvider. To make it globally available, use it at the top of the React tree. Change index.js according to the following to make the context available:

import React from 'react'
import ReactDOM from 'react-dom/client'
import './styles.css'

import { UserProvider } from './context.js'
import App from './App'

const user = {
    name: 'John',
    learning: 'React'
};

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <UserProvider value={user}>
            <App />
        </UserProvider>
    </React.StrictMode>
);
main.js Wrap App with the UserProvider
Copied to clipboard!

Now the context is available in our app, with the value of the user object. To use the context, run the following example to replace the contents of App.js:

import React, { useContext } from 'react'
import { UserContext } from './context'

const App = () => {
    const user = useContext(UserContext);

    return (
        <div className="welcome">
            <h1>πŸ‘‹ Welcome {user.name}</h1>
            <span>πŸ“– Currently learning {user.learning}</span>
        </div>
    )
}

export default App;
App.js How to use useContext Execute code
Copied to clipboard!

To use the useContext hook, we need to pass the context that we want to use, in our case, the UserContext that we have exported from context.js. Using the same syntax, we can access the context anywhere in any component, without having to pass props down the tree.

Default values

Note that the default value that we defined inside context.js is not used anywhere. Instead, the value prop of the Provider is used as the value for the context. This is because useContext always uses the closest provider above it. In case it is not found, it uses the default value.

Change the default value to the following, then remove UserProvider from index.jsΒ to verify that the default value is used instead.

import { createContext } from 'react'

const defaultUser = {
    name: 'Guest',
    learning: 'Unknown'
};

const UserContext = createContext(defaultUser);
const UserProvider = UserContext.Provider;

export { UserContext, UserProvider };
context.js Change the default value in the context
Copied to clipboard!

Keep in mind that the default value is only used if there is no matching provider. If we have the provider, but without a value prop, React will still try to use the provider. This means only the last example is valid:

// ❌ UserProvider missing the value prop
<UserProvider>
    <App />
</UserProvider>

// ❌ UserProvider is still missing the value prop
<UserProvider user={user}>
    <App />
</UserProvider>

// βœ”οΈ Always use a value prop for a provider
<UserProvider value={user}>
    <App />
</UserProvider>
Providers must always have a value prop
Copied to clipboard!

If you are getting undefined for a context, even with a default value, you likely have a provider without a value.

This behavior ensures that React won't break, even if we are missing a provider accidentally. It also helps with testing, as we don't have to set up providers for test cases.


Using Multiple Context

Of course, it is also possible to use as many providers in React as necessary. For example, when it doesn't make sense to use the same context for two different purposes, we can create another one. Create a new context called MetaContext inside context.jsΒ and export both the context and its provider.

import { createContext } from 'react'

const UserContext = createContext({});
const UserProvider = UserContext.Provider;

const MetaContext = createContext({});
const MetaProvider = MetaContext.Provider;

export {
    UserContext,
    UserProvider,
    MetaContext,
    MetaProvider
};
context.js Add a new context to context.js
Copied to clipboard!

Make sure to export the provider from the MetaContext variable. To expose the context to our application, wrap App into the MetaProvider with the following value:

import React from 'react'
import ReactDOM from 'react-dom/client'
import './styles.css'

import { UserProvider, MetaProvider } from './context.js'
import App from './App'

const user = {
    name: 'John',
    learning: 'React'
};

const meta = {
    site: 'Webtips',
    plan: 'PRO'
}

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <UserProvider value={user}>
            <MetaProvider value={meta}>
                <App />
            </MetaProvider>
        </UserProvider>
    </React.StrictMode>
);
index.js Add MetaProvider to index.js
Copied to clipboard!

In this example, the order in which the providers are defined doesn't matter, as long as they wrap the App component. Now we have another context available that we can reference with a different useContext hook:

import React, { useContext } from 'react'
import { UserContext, MetaContext } from './context'

const App = () => {
    const user = useContext(UserContext);
    const meta = useContext(MetaContext);

    return (
        <div className="welcome">
            <h1>πŸ‘‹ Welcome {user.name}</h1>
            <span>
                πŸ“– Currently learning {user.learning},
                on {meta.site} as {meta.plan}
            </span>
        </div>
    )
}

export default App;
App.js Using multiple useContext Execute code
Copied to clipboard!

Whenever the providers are starting to pile up, it is a common practice to create a component whose only purpose is to collect providers in one place. This way, we can keep index.js clean.

// Using a separate component for collecting providers in one place
const ApplicationProvider = ({ children }) => (
    <UserProvider value={user}>
        <MetaProvider value={meta}>
            {children}
        </MetaProvider>
    </UserProvider>
)

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <ApplicationProvider>
            <App />
        </ApplicationProvider>
    </React.StrictMode>
);
Outsourcing providers
Copied to clipboard!
Looking to improve your skills? Check out our interactive course to master React from start to finish.
Master React

Updating Context

Context can also change over time. For example, we might want to update our user object from various components. To achieve this, we need to combine our context with a useState hook. Change App.js to the following and remove the provider from index.js:

import React, { useState, useContext } from 'react'
import { UserProvider, UserContext } from './context'

const defaultUser = {
    name: 'John',
    learning: 'React'
};

const UpdateUser = () => {
    const [user, setUser] = useContext(UserContext);

    return (
        <button onClick={() => setUser({ ...user, name: 'Jane' })}>
            Update user
        </button>
    )
}

const App = () => {
    const [user, setUser] = useState(defaultUser);

    return (
        <UserProvider value={[user, setUser]}>
            <div className="welcome">
                <h1>πŸ‘‹ Welcome {user.name}</h1>
                <span>πŸ“– Currently learning {user.learning}</span>
                <div>
                    <UpdateUser />
                </div>
            </div>
        </UserProvider>
    )
}

export default App;
App.js Updating context using useState Execute code
Copied to clipboard!

Here, we are passing the defaultUser to a useState hook. Both the user state as well as the setUser updater function are passed as the value for the provider. This means we can use the useContext hook like it was the useState hook from the App component.

Click on the button to update the context from the UpdateUser component. This way, we can access and update the state from anywhere in our application using the same useContext hook.


Overriding Context

Last but not least, it is also possible to override the value of a context. For example, if we need to use a different value for a specific segment of the application, we can nest the same provider with different values:

<UserProvider value={user}>
    <UserProfile />
    <UserProvider value={admin}>
        <AdminDashboard />
    <UserProvider />
</UserProvider>
Overriding context values
Copied to clipboard!
Did you find this page helpful?
πŸ“š More Webtips
Frontend Course Dashboard
Master the Art of Frontend
  • check Unlimited access to hundred of tutorials
  • check Access to exclusive interactive lessons
  • check Remove ads to learn without distractions
Become a Pro

Recommended