Learn Computing from the Experts | The Rheinwerk Computing Blog

How to Use React's Context API for State Management

Written by Rheinwerk Computing | Feb 3, 2025 2:00:00 PM

The Context API allows you to access centrally stored information independently of the component hierarchy and without passing the information to child components via props.

 

The Context API of React helps you provide structures in the component tree without the need to use props to pass them through each layer from source to target. The use of the Context API is particularly suitable for larger applications in which different places access information. The interface is used by external packages (such as the React router or Redux) as well as in application development. Thus the Context API enables you to decouple your components more and avoid passing around props. While this means that component function signatures are simplified as you save props, it also means that complexity within your application may increase due to the use of different contexts.

 

In general, you should only use the Context API if you use a particular piece of information in multiple places within your application or if the source of a piece of information is several layers away from its actual use.

 

The Context API

The Context API of React provides a set of functions and components. The createContext function plays a central role as you use it to create a context. You can pass a default value to this function when you call it, which will be used by React if you don't assign an explicit value in your application.

 

The object returned by the createContext method contains two properties—Provider and Consumer. Both properties can be used as components. The Provider property can be used to set a value for the context. The Consumer property is used for read access to the context, where you must use the provider component to work with the context. However, you no longer use the Consumer component in a modern React application; instead, you access the context via the useContext function of the Hooks API.

 

Normally you generate the context object in a separate file using the createContext function. You can pass the context object a default value that React will use if no value is assigned elsewhere. The context can contain any number of objects, which are then available to you in the scope of the context. You use the Provider component to place the context in the component tree. Then you can access the context from all child components of the provider. Typically, you place the context near the root component of your application.

 

To illustrate the use of the Context API, let's first look at a simple example before we add the context to a larger application.

Creating a “Context” Object

First, you create a new file in your application called Context.js. The contents of this file are shown in this listing.

 

import { createContext } from 'react';

 

const Context = createContext(0);

export default Context;

 

The context is initialized with the value 0 in this case. You can access the resulting object in your application. This happens, for example, in the App component.

Integrating the Context

The Context object provides you with two components, Provider and Consumer. The Provider component provides the context for the component tree. In the next listing, you can see the integration into the App component:

 

import { useState } from 'react';

import Context from './Context';

 

function App() {

   const [counter, setCounter] = useState(0);

 

   function increment() {

      setCounter((prevState) => prevState + 1);

   }

 

   return (

      <Context.Provider value={counter}>

      <button onClick={increment}>increment</button>

      </Context.Provider>

   );

}

 

export default App;

 

In this example, you create a counter state in the App component. The increment function allows you to increase the value of the counter by the value 1. In the JSX structure of the component, you use the Provider component of the Context object and assign the counter value as a value using the value prop. In the click-handler function of the button element, you call the increment function so that a click on the button increases the counter by one. Currently, this action doesn’t yet affect the display. In the next step, you implement another component that accesses the context and displays the value.

Read Access to the Context

For read access to the context, you use the useContext function. To display the counter value, create a new component called Counter and save it in a file called Counter.jsx:

 

import { useContext } from 'react';

import Context from './Context';

 

function Counter() {

   const value = useContext(Context);

 

   return <div>Counter: {value}</div>;

}

 

export default Counter;

 

As you can see, you use the useContext function in combination with the context object you created via the createContext function. This gives the hook function access to the provider closest in the component tree and makes the value available to you. Now you still need include the component in the App component so that it gets displayed:

 

import { useState } from 'react';

import Context from './Context';

import Counter from './Counter';

 

function App() {

   const [counter, setCounter] = useState(0);

 

   function increment() {

      setCounter((prevState) => prevState + 1);

   }

 

   return (

      <Context.Provider value={counter}>

      <Counter />

      <button onClick={increment}>increment</button>

      </Context.Provider>

   );

}

 

export default App;

 

When you switch to the browser with this configuration, you can click on the button and the counter component will display the updated value. The most important difference is that you can access this value without passing it as a prop to the component.

 

Using the Context API in the Sample Application

This introductory example represented a simple use of the Context API. However, you can implement much more complex solutions with the context. So you can not only store simple values (like numbers in this example) in the context, but also objects, arrays, or functions. Next, you create a context where you store the array structure that the useState function returns to you. This allows you to access the state at various points, but also to modify it. This context is responsible for the management of book data records. In the listing below, you can see the source code that takes care of creating the context:

 

import { createContext } from 'react';

 

const BooksContext = createContext([null, () => {}]);

 

export default BooksContext;

 

By default, you set the context to an array with the null elements and an empty arrow function. These represent the two elements from useState. In the next step, you implement a component that creates the state, connects it to the context, and provides the context's provider. You name this file BooksProvider.jsx. The corresponding source code can be found in this listing.

 

import { useState } from 'react';

import BooksContext from './BooksContext';

 

function BooksProvider({ children }) {

   const [books, setBooks] = useState([]);

 

   return (

      <BooksContext.Provider value={[books, setBooks]}>

         {children}

      </BooksContext.Provider>

   );

}

 

export default BooksProvider;

 

Here you can see another use of the children prop of a component. This implementation allows you to integrate the BooksProvider component into your component tree, and React will take care of ensuring that the elements you insert between the opening and closing tags are correctly placed as child elements in the component and thus have access to the context.

 

Now you need to include the BooksProvider component in your application. As mentioned earlier, you usually place the context near the root component, or in this case, just inside the App component, as shown:

 

import BooksProvider from './BooksProvider';

import BooksList from './BooksList';

 

function App() {

   return (

      <BooksProvider>

         <BooksList />

      </BooksProvider>

   );

}

 

export default App;

 

The BooksList component is the first component in the tree that can access the context. As before, your task is to display the frame of the list and iterate over the individual data records. The next listing shows the source code of the component:

 

import { useEffect, useContext } from 'react';

import BooksContext from './BooksContext';

import BooksListItem from './BooksListItem';

import './BooksList.css';

import axios from 'axios';

 

function BooksList() {

   const [books, setBooks] = useContext(BooksContext);

   useEffect(() => {

      (async () => {

         const { data } = await axios.get(

            `${process.env.REACT_APP_API_SERVER}/books`

         );

         setBooks(data);

      })();

   }, []);

 

return (

   <table>

      <thead>

         <tr>

            <th>Title</th>

            <th>Author</th>

            <th>ISBN</th>

            <th></th>

         </tr>

      </thead>

      <tbody>

         {books.map((book) => (

            <BooksListItem key={book.id} book={book} />

         ))}

         </tbody>

      </table>

   );

}

 

export default BooksList;

 

The BooksList component uses the useContext function to gain access to the context. It loads the data from the server within an effect hook and writes it to the context using the setBooks function. In the JSX structure, the component iterates over the books array and creates new instances of the BooksListItem component for each data record. Here you pass the respective data record to the component for display. The code of the BooksListItem component is shown here:

 

import { useContext } from 'react';

import BooksContext from './BooksContext';

import produce from 'immer';

import { StarBorder, Star } from '@mui/icons-material';

 

function BooksListItem({ book }) {

   const [, setBooks] = useContext(BooksContext);

 

   function handleRate(id, rating) {

      setBooks((prevState) => {

         return produce(prevState, (draftState) => {

            draftState.map((book) => {

               if (book.id === id) {

                  book.rating = rating;

               }

            return book;

         });

      });

   });

}

 

   return (

      <tr>

         <td>{book.title}</td>

         <td>{book.author ? book.author : 'Unknown'}</td>

         <td>{book.isbn}</td>

         <td>

            {new Array(5).fill('').map((item, i) => (

               <button

                className="ratingButton"

                key={i}

                onClick={() => handleRate(book.id, i + 1)}

               >

                {book.rating < i + 1 ? <StarBorder /> : <Star />}

               </button>

            ))}

         </td>

      </tr>

   );

}

 

export default BooksListItem;

 

The component receives all information for display from its parent component. This means that no access to the context is required. However, the component includes the possibility to rate the books as well. This means that the component must also manipulate the data records. Previously, this task was performed by the parent component, which was also responsible for the state. In this case, you can get the setBooks function from the context, and the BooksListItem component can change the data itself. With the resulting state change, React updates the display, and the changed rating becomes visible to users.

 

When loading the setBooks function from the context, you see a special feature of the destructuring statement for arrays: if you aren’t interested in the first array element, you can put a comma at the beginning and access the second element directly. This omission of elements works for any number, but quickly becomes confusing, so you shouldn’t use this feature too excessively.

 

When creating the rating buttons, you’ll see another feature, but it isn’t React-specific: you use the array constructor to create a new array with five memory locations. In the next step, these will be filled with empty strings so that you can insert button elements via the map method in the final step. These elements call the handleRate function in their click handler, which takes care of communicating with the context. After examining the somewhat more extensive Context API, we now come to a practical and manageable feature of React: fragments.

 

Editor’s note: This post has been adapted from a section of the book React: The Comprehensive Guide by Sebastian Springer.