When building React applications, we typically share data across several components from parent to child via props. Passing data from parent to child components would be easy if just a few layers of components were involved.
As more components are introduced, things start to get complex, and keeping track of state and props can quickly become cumbersome.
The React Context API provides an interface that enables data sharing across components without using the props drilling approach.
In this tutorial, we are going to build a mini e-commerce store and walk through examples of how we can use the context API for sharing data across multiple components.
Steps we'll cover:
- What is a Context
- Project Setup
- Building the Product Listings
- Why and When Do we need the context API?
- Creating a Context
- Consuming the Context
- Share Data across components
What is a React Context API?
The React Context API allows us to store and retrieve data across multiple components without passing data from parent to child components.
The React context works basically in a two-way approach. You wrap all components that share similar data within the context provider as a parent component and access the data in the context via a Consumer or useContext hook.
To use the context API, you need to create a context by calling the createContext function with an optional default value when using JavaScript.
const defaultValue = { title: "Bag" };
const CartContext = React.createContext(defaultValue);
The above creates a CartContext, we can call the useContext hook to consume the data from the CartContext.
function App() {
const data = React.useContext(CartContext);
return (
<CartContext.Provider value={defaultValue}>
<AnotherComponent>
<div>{data.title}</div>
</AnotherComponent>
</CartContext.Provider>
);
}
Project Setup
To get started with the project, run the command
npx create-next-app react-context-tutorial --typescript
This will bootstrap a Next.js app with TypeScript. I like to use absolute paths when building next.js applications.
- Open the tsconfig.json file and add the highlighted line.
"baseUrl":"src"
- Create a new folder named
srcin the root directory of the app. Move the pages and styles folder into the
srcfolder.Change the path of the style in
_app.tsxto use 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
- Run the command
npm run devto start the app. - Open your browser on port 3000 (
http://localhost:3000). - You should see the default welcome to next.js message!
Building the Product Listings
This is where things start to get interesting. We are going to build the UIs for the product listing and share data across several components in this section.
Replace the styles in global.css with this CSS.
Copy and paste
productsdata below inside theproducts.tsfile.
// src/constants/products.ts
const products = [
{
id: 1,
title: "Grape",
},
{
id: 2,
title: "ice cream",
},
{
id: 3,
title: "Tangerine",
},
];
export default products;
- Create the
Productdata type structure for the product of the app inside aproduct.tsfile in thetypesdirectory.
// src/types/product.ts
export default interface Product {
id: number;
title: string;
}
- Replace the
index.tsxin thepagesdirectory with the following code.
//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;
If we reload our browser, we should see "Hello World" printed on the screen.
- Create the following files (favorites.tsx, product-list.tsx, product-item.tsx and product-details.tsx) in the
componentsdirectory.
// 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;
// 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;
// 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;
// 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
pagesdirectory 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 list of my favorite products.
Why and When Do we need the context API?
If we look at the Home component, we see it has favorites state that keeps track of the user's favorite products. This data is shared across several components.
The ProductDetails component calls the handleFavorite function to update the state in the Home component and the Favorites component reacts to each update of the favorites data.
To add a product as a favorite, we need to pass the favorites state and handleFavorite function as prop down to the ProductDetails component.
If the ProductDetails component becomes very complex and requires ten properties, we need to pass all ten properties as props to the said component. This can become quite complicated and that is basically how React works. We pass data through props from parent to child component.
How can we manage data across components that are far apart in the component tree? This prop drilling method of sharing data between components can become very complicated and inefficient as more properties are introduced and the component tree continues to grow.
The React Context API allows share data across multiple components without using the prop drilling approach.
Creating a Context
Creating a context is as easy as calling createContext function with a default value. If you don't know what type of data you want to store in the context, you can save an empty object as any data type.
import { createContext } from "react";
const ExampleContext = createContext({} as any);
- create a folder named
contextin thesrcdirectory. - create a file a named
example.context.tsxinside thecontextfolder and copy the code below.
//src/context/example.context.tsx
import React, { createContext, useContext } from "react";
const myData = { username: "Israel" };
export const ExampleContext = createContext(myData);
interface Props {
children: React.ReactNode;
}
export const ExampleProvider: React.FC<Props> = ({ children }) => {
return (
<ExampleContext.Provider value={{ username: "Chibuzor" }}>
{children}
</ExampleContext.Provider>
);
};
- We created an
ExampleContextwithmyDataas it default value. - We created
ExampleProvidercomponent, this will act as a parent component that we can wrap multiple components that will consume data from the context. - The
ExampleProviderreturns "ExampleContext.Provider" component with a value passed as prop to the "ExampleContext.Provider" . - The
ExampleContext.Provider, makes the value passed as prop available to all child components. - The
ExampleContext.Providermust have only a single prop called "value" and that value can have only one value. Here we pass an object that has a similar data structure with themyDatadefault value of the context when it was created.
Consuming the Context
We are going to use the useContext hook to consume the data from our ExampleContext.
- Update the
example.context.tsx
//src/context/example.context.tsx
import React, { createContext, useContext } from "react";
const myData = { username: "Israel" };
export const ExampleContext = createContext(myData);
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>;
};
- Update the
index.tsxin the page directory.
// 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>
<Favorites products={products} favorites={favorites} />
<ProductList
products={products}
favorites={favorites}
handleFavorite={handleFavorite}
/>
</main>
</div>
);
};
export default Home;
Reloading the browser should print "Hello Chibuzor" on the screen.
In the example.context.tsx, we called the useContext hook and passed the ExampleContext whose data we want to consume. The useContext hook returns the data from the ExampleContext and the Greet component renders the data without receiving the data via props from the ExampleProvider component.
Does your CRUD app need server state management?
Meet the headless, React-based solution to build sleek CRUD applications. With refine, you can be confident that your codebase will always stay clean and boilerplate-free.
Try refine to rapidly build your next CRUD project, whether it's an admin panel, dashboard, internal tool or storefront.
Share Data across components
In this section, we are going to refactor our code to use the React context.
- create a
product.context.tsxfile in thecontextfolder and copy the code below.
// 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);
- We moved the logic for handling favorites inside the
productReducer. - We initialized the
ProductContextwith a default value. - We export a
useProductfunction that exports the value of theProductContext. If we don't export theuseProductfunction, we would have to call theuseContextand passProductContextas its argument each time we want to consume theProductContextdata in any component. - 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;
//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;
// 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;
// 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;
// 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 we reload our browser, our app should work as expected.
Our web application works as before, only this time the data is shared via the React Context API. The React context has some performance benefits when used properly.
Conclusion
Okay, I promise, that is all there is to getting started with the React Context API.
In summary, to use a context.
- Create the context by calling
createContextfunction. - Make the Context.Provider the Parent component.
- Call the
useContexthook and pass the context as an argument.
Here is the link to the Github repo.
https://github.com/pseudoeazy/react-context-tutorial
Live URL for the complete app.
http://react-context-tutorial-sf2t.vercel.app/



Top comments (0)