How to Properly Use the Context API in React With Hooks
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.
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 };
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>
);
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;
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 };
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>
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.
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>
);
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;
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>
);
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;
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>
Rocket Launch Your Career
Speed up your learning progress with our mentorship program. Join as a mentee to unlock the full potential of Webtips and get a personalized learning experience by experts to master the following frontend technologies: