Sheet ⁨02⁩ · ⁨Blog⁩Surveyed ⁨2026⁩

Blog post image for React Context API for State Management - A practical look at the React Context API for state management, including how to build a simple shared state system with Next.js and TypeScript.

React Context API for State Management

Published: 09 Mins read14 Mins listen
Markdown for AI(opens in a new tab)

Introduction

Managing application state well can make or break a React project. React offers several options for state management, and the Context API is one of the most flexible. But what exactly is the React Context API, and how does it differ from Redux, another popular state management library? This post covers both questions, along with the capabilities and limitations of the React Context API.

What is Context API in React?

Understanding the fundamentals of Context API

The React Context API arrived in React 16.3. It gives you a way to share data between components without manually passing props through every level of the component tree. That matters most when deeply nested components need shared data, like user authentication status, application themes, or language preferences.

The Context API is built on two core components:

  1. <Provider>: This component makes data available to all descendant components. It accepts a value prop, which can be any data type you want to share, including objects or functions.
  2. <Consumer>: The Consumer component reads the data provided by the nearest <Provider> in the component hierarchy.

Data shared through Context API looks like props, but it is available globally within the context. Any component that needs it can read it directly, with no explicit prop passing from parent to child.

When should you use Context API?

You might be wondering why Context API should be your choice over traditional prop-passing. There are several situations where it earns its place:

  1. Eliminating prop drilling: In large, deeply nested component trees, manually passing props down multiple levels gets unwieldy and error-prone. Context API gives you one place to manage shared data instead.
  2. Global state management: When your application needs to read and change data from several places, Context API lets you set up a global state that’s easy to maintain and update.
  3. Themes and localization: Context API is a good fit for themes, user preferences, and localization settings, since those are usually needed in several sections of your application.
  4. Authentication: If you need to retain user authentication status and make it accessible to different parts of your application, Context API offers an effective solution.

Building a simple state management system with Context API

Creating a new Next.js project

To demonstrate the capabilities of Context API, we’ll build a simple state management system using Next.js. First, let’s create a new Next.js project by running the following command:

Terminal
# npm
npx create-next-app next-context-api
# yarn
yarn create next-app next-context-api
# pnpm
pnpx create-next-app next-context-api

Next, the command-line interface will prompt you to select a template for your project. For this tutorial, we’ll choose TypeScript as our preferred option.

Terminal
? Would you like to use TypeScript? › No / Yes # Yes
? Would you like to use ESLint? › No / Yes # Yes
? Would you like to use Tailwind CSS? › No / Yes # Yes
? Would you like to use `src/` directory? No / Yes # Yes
? Would you like to use App Router? (recommended) › No / Yes # Yes
? Would you like to customize the default import alias (@/*)? › No / Yes # Yes
? What import alias would you like configured? › @/* # keep the default

We’ll also install one additional dependency:

Terminal
# npm
npm install --save-dev prettier prettier-plugin-tailwindcss
# yarn
yarn add --D prettier prettier-plugin-tailwindcss
# pnpm
pnpm add --save-dev prettier prettier-plugin-tailwindcss

Once the project is created, navigate to the project directory and start the development server by running the following command:

Terminal
# npm
npm run dev
# yarn
yarn dev
# pnpm
pnpm dev

Cleaning up the project and organizing the file structure

Next, let’s clean up the project by removing the default files and folders that we won’t be using. We’ll also create a new folder structure to organize our project files.

Project Structure
Root
├── src
├── app
├── layout.tsx
└── page.tsx
├── assets
├── icons
└── favicon.ico
└── styles
└── globals.css
├── components
├── shared-state-child
└── index.tsx
├── shared-state-grand-child
└── index.tsx
├── shared-state-sibling
└── index.tsx
index.ts
└── providers
└── use-provider.tsx
├── .eslintrc.cjs
├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierrc.cjs
├── .yarnrc
├── next.config.mjs
├── package.json
├── postcss.config.cjs
├── README.md
├── tailwind.config.ts
├── tsconfig.json
└── yarn.lock

Note

You can find the starter code for this project on Starter Code branch.

Creating a custom provider for Context API

Now, let’s create a custom provider for our Context API. First, we’ll create a new file called use-provider.tsx inside the providers folder. Then, we’ll add the following code to this file:

~/src/providers/use-provider.tsx
'use client';
import React, {
type ReactNode,
type Context,
createContext,
useContext,
useState,
} from 'react';
const initialContext = <T,>() => new Map<string, T>();
const Context = createContext(initialContext());
type ProviderProps = {
children: ReactNode;
};
export const Provider = ({children}: ProviderProps) => (
<Context.Provider value={initialContext()}>{children}</Context.Provider>
);
const useContextProvider = <T,>(key: string) => {
const context = useContext(Context);
return {
set value(v: T) {
context.set(key, v);
},
get value() {
if (!context.has(key)) {
throw Error(`Context key '${key}' Not Found!`);
}
return context.get(key) as T;
},
};
};
export const useProvider = <T,>(key: string, initialValue?: T) => {
const provider = useContextProvider<Context<T>>(key);
if (initialValue !== undefined) {
const Context = createContext<T>(initialValue);
provider.value = Context;
}
return useContext(provider.value);
};
export const useSharedState = <T,>(key: string, initialValue?: T) => {
let state = undefined;
if (initialValue !== undefined) {
const _useState = useState;
state = _useState(initialValue);
}
return useProvider(key, state);
};

Let’s walk through the code above to see how it works. First, we create a new context using the createContext function. Then, we create a custom hook called useProvider that accepts two arguments: key and initialValue. The key argument is used to identify the context, while the initialValue argument is used to set the initial value of the context. Next, we create a custom hook called useSharedState that accepts the same arguments as the useProvider hook. This hook is used to create a shared state that can be accessed and modified by multiple components.

Using the custom provider in the application

Now, let’s use the custom provider we created in the previous step in our application. First, we’ll import the Provider component from the use-provider.tsx file. Then, we’ll wrap the Layout component with the Provider component. Finally, we’ll add the following code to the Layout component:

~/src/app/layout.tsx
import '@/assets/styles/globals.css';
// Context API
import {Provider} from '@/provider/use-provider';
import type {Metadata} from 'next';
import {Inter} from 'next/font/google';
import React, {type ReactNode} from 'react';
const inter = Inter({subsets: ['latin']});
export const metadata: Metadata = {
title: 'Next.js Context API',
description: 'Next.js Context API example with TypeScript to manage state.',
};
type RootLayoutProps = {
children: ReactNode;
};
export default function RootLayout({children}: RootLayoutProps) {
return (
<html lang={'en'}>
<Provider>
<body className={inter.className}>{children}</body>
</Provider>
</html>
);
}

Creating a shared state

Now, let’s create a shared state using the useSharedState hook. First, we’ll create a new file called index.tsx inside the components/shared-state-child folder. Then, we’ll add the following code to this file:

~/src/components/shared-state-child/index.tsx
'use client';
// components
import {SharedStateGrandChild} from '@/components';
// Context API
import {useSharedState} from '@/provider/use-provider';
import React, {Fragment} from 'react';
export const SharedStateChild = () => {
const [count] = useSharedState<number>('count');
return (
<Fragment>
<p className={'text-center text-xl font-semibold'}>
Shared State Child: {count}
</p>
<SharedStateGrandChild />
</Fragment>
);
};
export default SharedStateChild;

Next, we’ll create a new file called index.tsx inside the components/shared-state-grand-child folder. Then, we’ll add the following code to this file:

~/src/components/shared-state-grand-child/index.tsx
'use client';
// Context API
import {useSharedState} from '@/provider/use-provider';
import React, {Fragment} from 'react';
export const SharedStateGrandChild = () => {
const [count] = useSharedState<number>('count');
return (
<Fragment>
<p className={'text-center text-xl font-semibold'}>
Shared State Grand Child: {count}
</p>
</Fragment>
);
};
export default SharedStateGrandChild;

Finally, we’ll create a new file called index.tsx inside the components/shared-state-sibling folder. Then, we’ll add the following code to this file:

~/src/components/shared-state-sibling/index.tsx
'use client';
// Context API
import {useSharedState} from '@/provider/use-provider';
import React, {Fragment} from 'react';
export const SharedStateSibling = () => {
const [count] = useSharedState<number>('count');
return (
<Fragment>
<p className={'text-center text-xl font-semibold'}>
Shared State Sibling: {count}
</p>
</Fragment>
);
};
export default SharedStateSibling;

Creating a index.ts file inside the components folder and adding the following code to it:

~/src/components/index.ts
export {default as SharedStateChild} from '@/components/shared-state-child';
export {default as SharedStateGrandChild} from '@/components/shared-state-grand-child';
export {default as SharedStateSibling} from '@/components/shared-state-sibling';

Updating the shared state from the parent component or page

Now, let’s update the shared state from the parent component. First, we’ll create a new file called index.tsx inside the app folder. Then, we’ll add the following code to this file:

~/src/app/page.tsx
'use client';
// Context API
// components
import {SharedStateChild, SharedStateSibling} from '@/components';
import {useSharedState} from '@/provider/use-provider';
export default function Home() {
const [_, setCount] = useSharedState<number>('count', 0);
const increment = () => setCount((prev) => prev + 1);
const decrement = () => setCount((prev) => prev - 1);
const reset = () => setCount(0);
return (
<main className={'flex h-screen flex-col items-center justify-center'}>
<h1 className={'text-center text-4xl font-bold'}>Next.js Context API</h1>
<p className={'text-center text-xl font-semibold'}>
Count Example with Context API and TypeScript
</p>
<div className={'mt-8 flex flex-col items-center justify-center gap-4'}>
<SharedStateChild />
<SharedStateSibling />
<div className={'flex flex-row items-center justify-center gap-4'}>
<button
type={'button'}
className={
'rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700'
}
onClick={increment}
>
Increment
</button>
<button
type={'button'}
className={
'rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700'
}
onClick={decrement}
>
Decrement
</button>
<button
type={'button'}
className={
'rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700'
}
onClick={reset}
>
Reset
</button>
</div>
</div>
</main>
);
}

Testing the application

Finally, let’s test the application by running the following command:

Terminal
# npm
npm run dev
# yarn
yarn dev
# pnpm
pnpm dev

If everything works as expected, you should see the following output:

Next.js Context API Example

Note

You can find the final code for this project on Final Code branch.

Is Context API the same as Redux?

React Context API compared with Redux

Redux is a well-known state management library, and plenty of React applications use it. It gives you a structured, centralized way to manage application state. So is Context API just a Redux alternative? Here are the real differences between the two.

  1. Complexity: Redux is known for strict architectural rules, and that cuts both ways. It enforces one-directional data flow and requires actions and reducers. That helps larger applications and feels like overkill on smaller projects. Context API is lighter and more flexible, with a simpler entry point, which suits applications with modest state management needs.

  2. Ecosystem: Redux has a mature ecosystem with many extensions, middleware, and developer tools. It has been tested hard in the field and has a large community with answers for most problems. Context API is gaining popularity, but its ecosystem is not as broad. If you need the more complete toolset, Redux is still the preferred choice.

  3. Performance: Redux does well on performance through memoization and efficient state updates. Context API on its own does not optimize as much. Bring in memoization helpers like reselect and useMemo, though, and you can get solid performance out of Context API too.

  4. Learning curve: Redux has a steeper learning curve because of its strict conventions and the boilerplate that comes with them. Context API is more approachable, especially for developers new to state management in React. If you want something quick and uncomplicated, Context API is the one to reach for.

  5. State size: For applications with large, tangled state structures, Redux gives you a clear, structured approach through reducers and actions. Context API fits applications with smaller and simpler state management needs.

Picking the right tool

The choice between Context API and Redux depends on what your application actually demands. On a small to medium project where you want simplicity and a short learning curve, Context API is a strong choice. For large applications with complex state management needs, where a mature ecosystem earns its keep, Redux is still the better option. Sometimes a mix works best: Context API for simpler local state inside specific components, Redux for the overall application state.

What is the problem with Context API in React?

Understanding the limitations of Context API

The React Context API has real limitations for state management. Here are the challenges you’re likely to run into when using it.

  1. Propagation of updates: Context API re-renders every component consuming the context each time the provider’s value changes. With a deep component tree, that means re-renders you didn’t need. Memoization and component-level optimization ease it.

  2. No built-in middleware: Redux offers middleware for managing side effects and asynchronous actions, which many applications need. Context API has no built-in middleware, so you either add libraries or write your own handling for side effects.

  3. Debugging tools: Redux offers an extensive suite of developer tools that pay off when you are debugging. Context API has some developer tools, but not the same depth, so tracing data flow and debugging issues is harder.

  4. Global vs. local state: Context API is mainly designed for sharing global state. If your application needs components with local state that shouldn’t be shared with the whole application, that is less straightforward with Context API. Redux, which can handle local component state, gives you more control there.

  5. Handling complex state: For applications with complex state structures, Redux’s reducers and actions offer a clear and structured approach. With Context API you write more code to manage complex state well.

Frequently Asked Questions on Context API

Now that we’ve explored the fundamentals, compared Context API with Redux, and discussed its limitations, let’s address some common questions related to the React Context API:

Can Context API replace Redux for large applications?

It is technically possible, but Context API is usually not the best fit for large applications. Redux’s architecture, middleware, and developer tools are better equipped to handle the complexity often encountered in large applications.

Can Context API and Redux coexist in the same application?

Yes, you can use both Context API and Redux in a single application. Context API handles simpler local state inside specific components, while Redux takes care of global state and complex state structures.

What are some typical use cases for Context API?

Context API works well for global application state: user authentication, theme management, and localization. It also removes the need for prop drilling in deeply nested component structures.

Has Redux become obsolete now that Context API exists?

Redux has not become obsolete. It is still a valuable tool, particularly for large applications with complicated state management requirements. Context API is lighter and friendlier to beginners, but it is an alternative rather than a replacement.

Can functions and methods be shared via Context API?

Yes. Context API lets you share functions and methods, so you can pass behavior across components as well as data.

Conclusion

The React Context API is a solid addition to React’s state management options. It simplifies sharing data between components, removes prop drilling, and manages global application state efficiently. It won’t replace Redux in every case, but it’s a more accessible and lighter alternative, especially for smaller projects and simpler state management needs.

Knowing the strengths and limitations of each tool matters. Weigh your project’s requirements and pick accordingly, whether that’s Context API, Redux, or a combination of both.

References

Was this useful?

You might also enjoy

More posts on similar topics

React With Redux Toolkit

React With Redux Toolkit

Prerequisites This post assumes that you have a basic understanding of React and Redux, and it helps if you have some experience with React Hooks like useReducer. Introduction Nowadays, we

Setup Nextjs Tailwind CSS Styled Components with TypeScript

Setup Nextjs Tailwind CSS Styled Components with TypeScript

Introduction In this post, we will set up Nextjs, Tailwind CSS and Styled Components with TypeScript, using the following tools:Nextjs Tailwind CSS Styled Components TypeScriptPrere

Building a Customizable Image Slider in React Using Hooks, SCSS, and TypeScript

Building a Customizable Image Slider in React Using Hooks, SCSS, and TypeScript

Introduction In this tutorial, we will be building a customizable image slider in React using hooks, SCSS, and TypeScript. An image slider is a common UI element used in web applications to displa

Run TypeScript Without Compiling

Run TypeScript Without Compiling

Introduction In this post, I will show you how to run TypeScript without compiling it to JavaScript first. This is useful for debugging and testing. Set up a TypeScript project Step 1: cr

TypeScript vs. JSDoc: Static Type Checking in JavaScript Compared

TypeScript vs. JSDoc: Static Type Checking in JavaScript Compared

TL;DRTypeScript and JSDoc are two tools for static type checking in JavaScript. TypeScript offers a full type system, advanced features, and strict type checking. JSDoc provides lightweight

Get Started with Building ReactJS and Docker: A Complete Guide

Get Started with Building ReactJS and Docker: A Complete Guide

Introduction Docker is a powerful tool that allows developers to create, deploy, and run applications in a portable and scalable way. It uses containerization to encapsulate all the dependencies a

6 related posts