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?
- What is React Context API?
- Why React Context?
- Use cases of the React Context API
- How to use the React context API with functional components
- How to use the React context API with class components
- How to use the React context API in a Next.js project
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.
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:
...
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.
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 theproducts.ts
file.
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. Thetypes
directory doesn't exist yet. You need to first create it.
export default interface Product {
id: number;
title: string;
}
- Replace the contents of the
src/pages/index.tsx
file with the code below.
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. Thecomponents
directory doesn't exist yet. You need to create it yourself.
Show favorites.tsx file
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
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
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
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.
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.
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. Thecontext
directory doesn't exist yet. You need to create it yourself.
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 exportsProductContext
. If we don't export theuseProduct
function, we would have to call theuseContext
and passProductContext
as its argument each time we want to consume theProductContext
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
.
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
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
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
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
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.