How to Create and Update Context in React

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.

React context data management

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.

text
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

jsx
1import { createContext } from "react";
2
3// Without a default value
4export const UserContext = createContext({});
jsx
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:

UI Tree

text
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

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

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

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

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

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

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

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

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

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.