Skip to main content
A Guide to useContext and React Context API
17 min read

A Guide to useContext and React Context API

This article was last updated on January 30, 2024 to add prop drilling section and update explanation of React context and useContext hook.

When building React applications, we typically pass data from parent to child components via props. It would be easy if we had few layers of components. However, some components are deeply nested.

Things become complex as you introduce more components with several nesting levels. Keeping track of state and props can become cumbersome.

The React Context API provides functionality for passing data from a parent component to its descendants without prop drilling.

In this tutorial, you will learn the context API and build a mini e-commerce store to illustrate how to use the context API in a real-world application.


Steps we'll cover:

What is prop drilling?

React is a declarative, component-based UI framework. You will almost always need to compose two or more components when building UIs. In React, a parent component can primarily share data with its children via props.

However it becomes more complex if your app has several nesting levels and you have to pass data from the topmost to the innermost component.

As an example, assume ComponentA renders ComponentB and ComponentB renders ComponentC. If you want the topmost component to share data with the innermost component, you need to pass props from ComponentA to ComponentB and then finally from ComponentB to ComponentC.

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

return <ComponentB counter={counter} />;
}

function ComponentB({ counter }) {
return <ComponentC counter={counter} />;
}

function ComponentC({ counter }) {
return <p>{counter}</p>;
}

In React, prop drilling refers to passing data via props through multiple layers of nested components, as in the above example. Props are useful for basic data sharing between a component and its children.

However, drilling props through several layers of nesting can make your components less readable, difficult to maintain, and reuse. You can solve this prop drilling problem using the React context API.

What is React Context API?

The React Context API is one of the built-in APIs in React. You can use it to pass data from a parent component to its descendants without prop drilling.

You can create a context by invoking the createContext function with an optional default value as in the example below. The default argument can be of any type.

import { createContext } from "react";
const ContextObject = createContext(defaultValue);

The createContext function returns a context object. The returned object has the Provider and Consumer properties. These properties are React components and are usually referred to as context providers and consumers respectively.

You can use the context provider to wrap the nested components that need to access the context data like so:

import { createContext, useContext, useState } from "react";

const Context = createContext(1);

function App() {
const [count, setCount] = useState(1);

return (
<>
<Context.Provider value={count}>
<NestedComponent />
</Context.Provider>
</>
);
}

As in the above example, you can use the value attribute of the context Provider component to make the context data available to the nested components .

Any nested functional component, no matter the nesting level, will be able to access or consume the context data via the useContext hook.

import { useContext } from "react";
import { Context } from "./App";

function NestedComponent() {
const contextValue = useContext(Context);

return <p>{contextValue}</p>;
}

Instead of using the useContext hook to consume context, you can also use the Context.Consumer component as in the example below.

function NestedComponent() {
return (
<>
<Context.Consumer>
{(contextValue) => {
return <p>{contextValue}</p>;
}}
</Context.Consumer>
</>
);
}

However, this is a legacy method for consuming context. Always stick with the useContext hook in functional components. Use the Context.Consumer method in class components where you can't use hooks.


Why React Context?

As hinted above, one of the primary reasons for using the context API is to reduce the downsides of prop drilling. Passing props through deeply nested component tree can result in complex, tightly coupled, and difficult-to-maintain code.

The context API comes in handy if your code requires passing data through several levels of component hierarchy. It makes your code easier to read and maintain.

Use cases of the React Context API

There are several use-cases of the React context API. Below are some of the use-cases you might encounter often.

Managing application theme

Most websites and web apps have built-in theme switching functionality. In managing application theme, you can wrap the root component in a theme context provider. Any nested component in the component hierarchy can read the context value.

A user can switch the theme and the context provider will make the currently selected theme available to all its descendants.

Managing user authentication

You can use the context API to manage user authentication. You can provide information about the currently logged in user to all components via context.

Managing localization

Most modern applications may need to support multiple languages so that a user can switch to a language they know. You can wrap the root component in a context provider. Any nested component can read the selected language and render content in the user's locale of choice.

Manage routing

In the React ecosystem, most routing packages rely on the context API to hold information about the active route. Therefore, if you've ever used one of the front-end routing libraries, chances are high that it uses the context API under the hood.

How to use the React context API with functional components

In this section, we will explore how to use the context API in React functional components.

How to create Context in React

As hinted above, you can create context by invoking the built-in createContext function with a default value as an argument.

import { createContext } from "react";
const ExampleContext = createContext({} as any);

In most real-world applications, you may want to create a dedicated directory for managing your application's context. You can create context in this directory and import them for your consumers and providers. Creating such a directory simplifies code maintenance.

The code below illustrates how to create a basic React context.

import React, { createContext } from "react";

export const ExampleContext = createContext({ username: "Israel" });

interface Props {
children: React.ReactNode;
}

export const ExampleProvider: React.FC<Props> = ({ children }) => {
return (
<ExampleContext.Provider value={{ username: "Chibuzor" }}>
{children}
</ExampleContext.Provider>
);
};

In the code above, we created a simpleExampleContext with a default value. The ExampleProvider component is our parent component. It will wrap multiple nested components that want to consume the data shared by the context.


How to consume Context in React

Any component nested within a context provider can consume the shared data. In React functional components, you can use the useContext hook to read the context value. The useContext hook takes the context as an argument and returns the context value as in the example below.

src/context/example.context.tsx
import React, { createContext, useContext } from "react";

export const ExampleContext = createContext({ username: "Israel" });

interface Props {
children: React.ReactNode;
}

export const ExampleProvider: React.FC<Props> = ({ children }) => {
return (
<ExampleContext.Provider value={{ username: "Chibuzor" }}>
{children}
</ExampleContext.Provider>
);
};

export const Greet = () => {
const data = useContext(ExampleContext);
return <h1>Hello, {data.username}</h1>;
};

In the code above, we declared a simple context provider and a component that will consume the context data. The two components don't need to be in the same file.

You can now import and use the components we created above like so:

src/pages/index.tsx
...
import { ExampleProvider, Greet } from "context/example.context";

const Home: NextPage = () => {
...
return (
<div className="container">
<main className="main-content">
<ExampleProvider>
<Greet />
</ExampleProvider>
</main>
</div>
);
};

export default Home;

The abvoe component should now display the text "Hello Chibuzor". That's just about everything you need to know to create and consume context in React functional components.


How to use the React context API with class components

Creating context in React class components is similar to that in functional components. You use the createContext function to create context and wrap your components in the context provider.

However, the difference is in consuming context because hooks only work with functional components. Therefore, you can't use the useContext hook to consume context in class components.

With React class components, you use the Context.Consumer component to consume the context data as in the example below.

import { Component } from "react";
import { Context } from "./Context";

class MyComponent extends Component {
render() {
return (
<>
<Context.Consumer>
{(contextValue) => {
return <p>{contextValue}</p>;
}}
</Context.Consumer>
</>
);
}
}

How to use the React context API in a Next.js project

In this section, we will build a simple e-commerce site using the context API.

Set up the project

Run the command below to set up a simple Next.js project.

npx create-next-app react-context-tutorial --typescript

The command above will bootstrap a Next.js app with TypeScript. Select the following options when prompted on the commandline.

✔ Would you like to use ESLint with this project? Yes
✔ Would you like to use `src/` directory with this project? Yes
✔ Would you like to use experimental `app/` directory with this project? No
✔ What import alias would you like configured? @/*
  • Open the tsconfig.json file and add the changes below to it.

    {
    "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "baseUrl": "src",
    "paths": {
    "@/*": ["./src/*"]
    }
    },
    "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
    "exclude": ["node_modules"]
    }
  • Import styles in the src/_app.tsx file using absolute path.

src/_app.tsx
import "styles/globals.css";
import type { AppProps } from "next/app";

function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}

export default MyApp;
  • Start the development server using the npm run dev command
  • Open your browser on localhost on port 3000

Building the Product Listings

In this section, we will build the UIs for the product list and share data across several components.

  • Replace the styles in the styles/globals.css file with the styles below.

    Show the styles/globals.css file

    styles/globals.css
    /**  css reset **/
    *,
    *::before,
    *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    }
    body {
    font-family: Arial, Helvetica, sans-serif;
    }

    /*main page content*/
    .main-content {
    display: flex;
    flex-direction: column;
    min-height: 80vh;
    margin-top: 100px;
    }

    /*product*/
    .product-container {
    width: 800px;
    margin: 16px auto;
    }

    .product-details {
    display: flex;
    margin-top: 2em;
    }

    .product-image,
    .product-info {
    padding: 10px;
    }

    .product-image {
    flex: 1;
    font-size: 6em;
    border: 1px solid #999;
    }

    table.product-info {
    border-collapse: collapse;
    flex: 2;
    }

    .product-info td {
    border: 1px solid #999;
    padding: 0.5rem;
    text-align: left;
    letter-spacing: 2px;
    }

    .product-info td:last-child {
    line-height: 18px;
    }

    .additional-info {
    border-left: 2px solid purple;
    padding-left: 4px;
    margin-bottom: 4px;
    }

    .add-to-cart {
    display: flex;
    justify-content: flex-end;
    }
    .favorites h2 {
    margin-bottom: 32px;
    }
    .favorites {
    max-width: 600px;
    margin: 32px auto;
    text-align: center;
    }

    .favorites ul {
    text-align: left;
    margin: 16px;
    }

    .button {
    padding: 4px;
    margin: 4px;
    font-size: 20px;
    cursor: pointer;
    }

    .update-quantity {
    width: 124px;
    display: flex;
    padding: 2px;
    margin-top: 0.4em;
    background-color: #fdfefe;
    box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.5);
    }
    .quantity {
    width: 40px;
    text-align: center;
    font-size: 20px;
    }
    .update-button {
    width: 40px;
    padding: 4px;
    font-size: 20px;
    border: none;
    background-color: transparent;
    cursor: pointer;
    }

  • Copy and paste the products data below in the products.ts file.
src/constants/products.ts
const products = [
{
id: 1,
title: "Grape",
},
{
id: 2,
title: "ice cream",
},
{
id: 3,
title: "Tangerine",
},
];

export default products;
  • Create the src/types/product.ts file. Copy and paste the code below into it. The types directory doesn't exist yet. You need to first create it.
src/types/product.ts
export default interface Product {
id: number;
title: string;
}
  • Replace the contents of the src/pages/index.tsx file with the code below.
src/pages/index.tsx
import type { NextPage } from "next";

const Home: NextPage = () => {
return (
<div className="container">
<main className="main-content">
<h1 style={{ textAlign: "center" }}>Hello World</h1>
</main>
</div>
);
};

export default Home;

The text "Hello World" will be displayed on the screen.


  • Create the favorites.tsx, product-list.tsx, product-item.tsx, and product-details.tsx files in the src/components directory. The components directory doesn't exist yet. You need to create it yourself.
Show favorites.tsx file

src/components/favorites.tsx
import Product from "types/product";

interface Props {
products: Product[];
favorites: number[];
}

const Favorites: React.FC<Props> = ({ products, favorites }) => {
const myFavorites: Product[] = [];

favorites.forEach((fav) => {
const favorite = products.find((product) => product.id === fav);
if (favorite) {
myFavorites.push(favorite);
}
});

return (
<section className="favorites">
<h2>My Favorite products</h2>
{myFavorites.length ? (
<ul>
{myFavorites.map((favorite) => (
<li key={favorite.id}>{favorite.title}</li>
))}
</ul>
) : (
<div>😂No favorite product!</div>
)}
</section>
);
};
export default Favorites;

Show product-list.tsx file

src/components/product-list.tsx
import React from "react";
import ProductItem from "components/product-item";
import Product from "types/product";

interface Props {
favorites: number[];
products: Product[];
handleFavorite: (productId: number) => void;
}

const ProductList: React.FC<Props> = ({
favorites,
products,
handleFavorite,
}) => {
return (
<section className="product-container">
{products.map((product) => (
<ProductItem
key={product.id}
product={product}
handleFavorite={handleFavorite}
favorites={favorites}
/>
))}
</section>
);
};
export default ProductList;

Show product-item.tsx file

src/components/product-item.tsx
import React from "react";
import ProductDetails from "components/product-details";
import Product from "types/product";

interface Props {
product: Product;
favorites: number[];
handleFavorite: (productId: number) => void;
}

const ProductItem: React.FC<Props> = ({
product,
handleFavorite,
favorites,
}) => {
return (
<div className="product-card">
<ProductDetails
product={product}
handleFavorite={handleFavorite}
favorites={favorites}
/>
</div>
);
};
export default ProductItem;

Show product-details.tsx file

src/components/product-details.tsx
import React from "react";
import Product from "types/product";

interface Props {
product: Product;
favorites: number[];
handleFavorite: (productId: number) => void;
}

const ProductDetails: React.FC<Props> = ({
product,
handleFavorite,
favorites,
}) => {
const isFavorite = favorites.includes(product.id);
return (
<div className="product-details-container">
<div className="product-details">
<div className="product-image">{product.title}</div>
</div>
<div className="add-to-cart">
<button
type="button"
className="button"
onClick={() => handleFavorite(product.id)}
>
<span>{isFavorite ? "❤️" : "❤︎"}</span>
</button>
</div>
</div>
);
};
export default ProductDetails;

  • Update the index.tsx file in the pages directory with the following code.
src/pages/index.tsx
import { useState } from "react";
import type { NextPage } from "next";
import ProductList from "components/product-list";
import products from "constants/products";
import Favorites from "components/favorites";

const Home: NextPage = () => {
const [favorites, setFavorites] = useState<number[]>([]);

const handleFavorite = (productId: number) => {
if (favorites.includes(productId)) {
const newFavorites = favorites.filter((fav) => fav !== productId);
setFavorites(newFavorites);
} else {
setFavorites([...favorites, productId]);
}
};
return (
<div className="container">
<main className="main-content">
<Favorites products={products} favorites={favorites} />
<ProductList
products={products}
favorites={favorites}
handleFavorite={handleFavorite}
/>
</main>
</div>
);
};

export default Home;

If you click the favorite icon for each product, you should see it listed under the "My Favorite products" list.

React context API favoriteProduct

Share Data using the context API

In this section, we will refactor our code to use the React context API.

  • Create the context/product.context.tsx file. Copy and paste the code below into it. The context directory doesn't exist yet. You need to create it yourself.
src/context/product.context.tsx
import React, { createContext, useContext, useReducer } from "react";
import products from "constants/products";
import Product from "types/product";

type ProductData = {
products: Product[];
favorites: number[];
};

type ProductAction =
| {
type: "PRODUCTS";
products: Product[];
}
| {
type: "FAVORITES";
favorites: number;
};

const productReducer = (
state: ProductData,
action: ProductAction,
): ProductData => {
switch (action.type) {
case "PRODUCTS":
return { ...state, products: action.products };
case "FAVORITES":
let favorites = state.favorites;

if (state.favorites.includes(action.favorites)) {
favorites = favorites.filter((fav) => fav !== action.favorites);
} else {
favorites = [...state.favorites, action.favorites];
}

return { ...state, favorites };
default:
return state;
}
};

const defaultValues: ProductData = {
products,
favorites: [],
};

const myProduct = {
product: defaultValues,
setProduct: (action: ProductAction): void => {},
};

const ProductContext = createContext<{
product: ProductData;
setProduct: React.Dispatch<ProductAction>;
}>(myProduct); //initialize context with default value

interface Props {
children: React.ReactNode;
}

export const ProductProvider: React.FC<Props> = ({ children }) => {
const [product, setProduct] = useReducer(productReducer, defaultValues);

return (
<ProductContext.Provider value={{ product, setProduct }}>
{children}
</ProductContext.Provider>
);
};

export const useProduct = () => useContext(ProductContext);
  • In the code above, we moved the logic for handling favorites into the productReducer.
  • We initialized the ProductContext with a default value.
  • We exported a useProduct function that exports ProductContext. If we don't export the useProduct function, we would have to call the useContext and pass ProductContext as its argument each time we want to consume the ProductContext data.
  • We need to update our components (index.tsx, favorites.tsx, product-list.tsx, product-item.tsx and product-details.tsx) to consume the data in the ProductContext.
src/pages/index.tsx
import type { NextPage } from "next";
import ProductList from "components/product-list";
import Favorites from "components/favorites";
import { ProductProvider } from "context/product.context";

const Home: NextPage = () => {
return (
<div className="container">
<main className="main-content">
<ProductProvider>
<Favorites />
<ProductList />
</ProductProvider>
</main>
</div>
);
};

export default Home;

Show the favorites.tsx file

src/components/favorites.tsx
import Product from "types/product";
import { useProduct } from "context/product.context";

const Favorites: React.FC = () => {
const { product } = useProduct();
const myFavorites: Product[] = [];

product.favorites.forEach((fav) => {
const favorite = product.products.find((product) => product.id === fav);
if (favorite) {
myFavorites.push(favorite);
}
});

return (
<section className="favorites">
<h2>My Favorite products</h2>
{myFavorites.length ? (
<ul>
{myFavorites.map((favorite) => (
<li key={favorite.id}>{favorite.title}</li>
))}
</ul>
) : (
<div>😂No favorite product!</div>
)}
</section>
);
};

export default Favorites;

Show the product-list.tsx file

src/components/product-list.tsx
import React from "react";
import ProductItem from "components/product-item";
import { useProduct } from "context/product.context";

const ProductList: React.FC = () => {
const { product } = useProduct();
return (
<section className="product-container">
{product.products.map((product) => (
<ProductItem key={product.id} product={product} />
))}
</section>
);
};

export default ProductList;

Show the product-item.tsx file

src/components/product-item.tsx
import React from "react";
import ProductDetails from "components/product-details";
import Product from "types/product";

interface Props {
product: Product;
}
const ProductItem: React.FC<Props> = ({ product }) => {
return (
<div className="product-card">
<ProductDetails product={product} />
</div>
);
};

export default ProductItem;

Show the product-details.tsx file

src/components/product-details.tsx
import React from "react";
import Product from "types/product";
import { useProduct } from "context/product.context";

interface Props {
product: Product;
}

const ProductDetails: React.FC<Props> = ({ product }) => {
const { product: productData, setProduct } = useProduct();

const handleFavorite = (productId: number) => {
setProduct({ type: "FAVORITES", favorites: productId });
};

const isFavorite = productData.favorites.includes(product.id);

return (
<div className="product-details-container">
<div className="product-details">
<div className="product-image">{product.title}</div>
</div>
<div className="add-to-cart">
<button
type="button"
className="button"
onClick={() => handleFavorite(product.id)}
>
<span>{isFavorite ? "❤️" : "❤︎"}</span>
</button>
</div>
</div>
);
};

export default ProductDetails;

  • If you reload the browser, the app should work as expected.

Our web application works as before, only this time the data is shared via the React Context API.


Conclusion

The context API comes in handy when passing data from a parent component to its deeply nested descendants.

In a typical React app, you create context by invoking the built-in createContext function. It takes an initial value as an argument and returns a context object. The returned context object has a context provider and a context consumer.

You wrap your components in a context provider and use the value attribute to share the context with the nested components that need it.

The nested components can access the context data using the useContext hook. The useContext hook takes the context object as argument.