Certain information in your application needs to be available to multiple components, including those nested deep in the UI tree. However, passing data through layers of components via props can quickly become tedious and difficult to maintain as the application grows.
To simplify data management, context allows us to define information in the parent component, and they will be automatically accessible to all the components down the tree.
Creating the context
To get started with context, navigate to the contexts
directory, and create a new file called UserContext.jsx
. This file will define the user context and allow different components to access user data.
1src
2├── App.jsx
3├── assets
4├── components
5├── contexts
6│ └── UserContext.jsx <=====
7└── main.jsx
Inside UserContext.jsx
, initialize the context using the createContext()
function. TIt can be initialized with or without a default value.
contexts/UserContext.jsx
1import { createContext } from "react";
2
3// Without a default value
4export const UserContext = createContext({});
1import { createContext } from "react";
2
3// With default value
4export const UserContext = createContext({
5 username: "John Doe",
6 email: "johndoe@example.com",
7 status: "active",
8});
When a default value is specified, it will be passed to the child components if the parent component doesn't provide one, as we will demonstrate later.
Setting up provider in the parent
To demonstrate how the context can be passed down, let's first create an UI tree with the following structure:
1src
2├── App.jsx
3├── assets
4│ └── react.svg
5├── components
6│ ├── Hello.jsx <=====
7│ ├── Profile.jsx <=====
8│ └── Section.jsx <=====
9├── contexts
10│ └── UserContext.jsx
11└── main.jsx
Our goal is to supply some information in the App.jsx
, and make sure it is accessible in the leaf nodes (Hello
and Section
).
There are two things we need to do to accomplish this. First of all, you need to have a provider to supply the data:
App.jsx
1import Profile from "./components/Profile";
2import { UserContext } from "./contexts/UserContext"; // Import UserContext
3
4export default function App() {
5 // Assuming this user data is retrieved from the database or a remote source
6 const user = {
7 username: "Ethan Blake",
8 email: "ethanblake@example.com",
9 status: "inactive",
10 };
11
12 return (
13 // user will be passed to all child components inside UserContext.Provider
14 <UserContext.Provider value={user}>
15 <Profile />
16 </UserContext.Provider>
17 );
18}
Realistically, the user data will be retrieved from the database or some kind of external data store. For demonstration purposes, we are hardcoding the user
, and in this example, it will replace the default value we defined previously, and passed to all child components inside <UserContext.Provider>
.
components/Profile.jsx
1import Hello from "./Hello";
2import Section from "./Section";
3
4export default function Profile() {
5 return (
6 <>
7 <Hello />
8 <Section />
9 </>
10 );
11}
Accessing context in the child
To access the context, go into the leaf component where you want the context to be available, and use the useContext()
hook to access the context. For example, you can access the username
from UserContext
like this:
components/Hello.jsx
1import { useContext } from "react";
2import { UserContext } from "../contexts/UserContext";
3
4export default function Hello() {
5 const { username } = useContext(UserContext);
6 return <p>Welcome, {username}!</p>;
7}
Recall that UserContext
contains an object with default value like this:
contexts/UserContext.jsx
1import { createContext } from "react";
2
3export const UserContext = createContext({
4 username: "John Doe",
5 email: "johndoe@example.com",
6 status: "active",
7});
But in the App.jsx
, through the context provider, the default value will be replaced with user
:
App.jsx
1import Profile from "./components/Profile";
2import { UserContext } from "./contexts/UserContext"; // Import UserContext
3
4export default function App() {
5 // Assuming this user data is retrieved from the database
6 const user = {
7 username: "Ethan Blake",
8 email: "ethanblake@example.com",
9 status: "inactive",
10 };
11
12 return (
13 // user will be passed to all child components inside UserContext.Provider
14 <UserContext.Provider value={user}>
15 <Profile />
16 </UserContext.Provider>
17 );
18}
So eventually, the username
passed to the leaf nodes should be "Ethan Blake"
.
And for the Section
component, things wok exactly the same, except you need to retrieve the email
and status
as well.
components/Section.jsx
1import { useContext } from "react";
2import { UserContext } from "../contexts/UserContext";
3
4export default function Section() {
5 const { username, email, status } = useContext(UserContext);
6 return (
7 <>
8 <p>User Name: {username}</p>
9 <p>Email: {email}</p>
10 <p>Status: {status}</p>
11 </>
12 );
13}
Using context with state
Contexts are often used in combination with state to create a reactive system where state updates are propagated to multiple components within the UI tree.
To achieve this, our context provider needs to be extended, so that it can manage state and pass both the state variable and its setter function down through the context.
This allows any component consuming the context to access and modify this shared state.
contexts/UserContext.jsx
1import { createContext, useState } from "react";
2
3export const UserContext = createContext({
4 username: "John Doe",
5 email: "johndoe@example.com",
6 status: "active",
7 setUser: () => {}, // Placeholder
8});
9
10export function UserProvider({ children }) {
11 const [user, setUser] = useState({
12 username: "Ethan Blake",
13 email: "ethanblake@example.com",
14 status: "active",
15 });
16
17 return (
18 <UserContext.Provider value={{ ...user, setUser }}>
19 {children}
20 </UserContext.Provider>
21 );
22}
In this example, the UserProvider
acts as a wrapper around UserContext.Provider
, extending its functionalities by declaring the state user
. And instead of directly using UserContext.Provider
in your App.jsx
, you can replace it with UserProvider
.
App.jsx
1import Profile from "./components/Profile";
2import { UserProvider } from "./contexts/UserContext";
3
4export default function App() {
5 return (
6 <UserProvider>
7 <Profile />
8 </UserProvider>
9 );
10}
And finally, inside the component that consumes the context, you can access the state setter function setUser()
, and use it to update user
to a new value.
components/Section.jsx
1import { useContext } from "react";
2import { UserContext } from "../contexts/UserContext";
3
4export default function Section() {
5 const { username, email, status, setUser } = useContext(UserContext);
6
7 function handleUserUpdate() {
8 setUser({
9 username: "Updated User",
10 email: "updated@example.com",
11 status: "inactive",
12 });
13 }
14
15 return (
16 <>
17 <p>User Name: {username}</p>
18 <p>Email: {email}</p>
19 <p>Status: {status}</p>
20
21 <label htmlFor="username">Change Username:</label>
22 <input
23 id="username"
24 name="username"
25 type="text"
26 value={username}
27 onChange={(e) => {
28 setUser({
29 username: e.target.value,
30 email: email,
31 status: status,
32 });
33 }}
34 />
35 </>
36 );
37}
This new value will be updated in the context, and then be propagated to Hello
as well.