DEV Community

Cover image for A Guide to useContext and React Context API
Necati Özmen for Refine

Posted on • Originally published at refine.dev

A Guide to useContext and React Context API

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 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);
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Project Setup

To get started with the project, run the command

npx create-next-app react-context-tutorial --typescript 
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode
  • Create a new folder named src in the root directory of the app.
  • Move the pages and styles folder into the src folder.

  • Change the path of the style in _app.tsx to 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

Enter fullscreen mode Exit fullscreen mode
  • Run the command npm run dev to 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 products data below inside 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;
Enter fullscreen mode Exit fullscreen mode
  • Create the Product data type structure for the product of the app inside a product.ts file in the types directory.
// src/types/product.ts
export default interface Product {
  id: number;
  title: string;
}
Enter fullscreen mode Exit fullscreen mode
  • Replace the index.tsx in the pages directory 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;
Enter fullscreen mode Exit fullscreen mode

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 components directory.
// 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;
Enter fullscreen mode Exit fullscreen mode
// 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;
Enter fullscreen mode Exit fullscreen mode
// 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;
Enter fullscreen mode Exit fullscreen mode
// 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;

Enter fullscreen mode Exit fullscreen mode

  • 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;

Enter fullscreen mode Exit fullscreen mode

If you click the favorite icon for each product, you should see it listed under the list of my favorite products.

Image test

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.

Image context

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);
Enter fullscreen mode Exit fullscreen mode
  • create a folder named context in the src directory.
  • create a file a named example.context.tsx inside the context folder 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>
  );
};
Enter fullscreen mode Exit fullscreen mode
  • We created an ExampleContext with myData as it default value.
  • We created ExampleProvider component, this will act as a parent component that we can wrap multiple components that will consume data from the context.
  • The ExampleProvider returns "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.Provider must 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 the myData default 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>;
};

Enter fullscreen mode Exit fullscreen mode
  • Update the index.tsx in 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;

Enter fullscreen mode Exit fullscreen mode

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.


refine blog logo

Share Data across components

In this section, we are going to refactor our code to use the React context.

  • create a product.context.tsx file in the context folder 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);

Enter fullscreen mode Exit fullscreen mode
  • We moved the logic for handling favorites inside the productReducer.
  • We initialized the ProductContext with a default value.
  • We export a useProduct function that exports the value of the 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 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;
Enter fullscreen mode Exit fullscreen mode

//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;
Enter fullscreen mode Exit fullscreen mode
// 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;
Enter fullscreen mode Exit fullscreen mode
// 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;
Enter fullscreen mode Exit fullscreen mode
// 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;
Enter fullscreen mode Exit fullscreen mode
  • 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 createContext function.
  • Make the Context.Provider the Parent component.
  • Call the useContext hook and pass the context as an argument.

Here is the link to the Github repo.

https://github.com/pseudoeazy/react-context-tutorial
Enter fullscreen mode Exit fullscreen mode

Live URL for the complete app.

http://react-context-tutorial-sf2t.vercel.app/
Enter fullscreen mode Exit fullscreen mode

Top comments (0)