
How to use the useContext Hook in React?
Learn everything about the useContext hook in React - prop drilling, useContext examples, useContext vs redux, useContext and useReducer, interview questions, useContext in React 19, and more!

Kaushal Joshi
May 22, 2025 • 12 min read
React is known for its component-based architecture, which makes building user interfaces modular and reusable. However, managing states and passing data between deeply nested components can become cumbersome as your application scales. React provides a hook called useContext
to resolve this problem.
In this article, we will explore the useContext hook in React. We will start by understanding why we need it. We will then learn how to create a context, provide it to the components, and consume it whenever necessary. We will also see some real-world examples of the useContext hook. Finally, we will conclude with interview questions to help you prepare for your interviews.
Prerequisites and Assumptions
While writing this article, I am presuming that you are already familiar with the following topics:
Component-based architecture
States and props
When a component rerender
Built-in React hooks
If you are unaware of these, I request you to save this article for later and learn the basics of React first. react.dev/learn is a good starting place to learn React.
Let's start with understanding what the useContext hook in React is all about.
What is the useContext Hook in React?
The useContext hook in React allows components to consume data without manually passing props at each level. It enables components to access shared data without passing props manually through multiple levels of components. This is particularly useful in larger applications where prop drilling can make the codebase harder to maintain.
What is prop drilling in React?
React follows unidirectional data flow. Data may only passed from parent components to child components as props. Therefore, a deeply nested child component may occasionally require data stored in a parent component.
If the subsequent child components also need the same data, then there's no problem in passing data as a prop from one component to another. However if it doesn't need it, it counts as an additional payload for the component that could affect its performance. This is called 'prop drilling'.
When passing data down multiple levels in a component tree, each intermediate component must pass the data as props, even if they don't need it.
const App = () => {
const user = { name: "Janardan", surname: "Doekar" };
return <Parent user={user} />;
};
const Parent = ({ user }) => {
return <Child user={user} />;
};
const Child = ({ user }) => {
return <GrandChild user={user} />;
};
const GrandChild = ({ user }) => {
return <p>{user.name} {user.surname}</p>;
};
In this example, even though only <GrandChild />
needs the user
data, every intermediate component must pass it down, leading to unnecessary prop drilling.
How the useContext hook solves prop drilling
With the useContext
hook, we can eliminate prop drilling by providing shared data at a higher level and consuming it directly where needed. This completely eliminates the need to share props from <Parent />
component to <Child />
and later pass it down to <GrandChild />
. The <GrandChild />
can directly consume the data if it wants.
Step 1: Create a Context
Let's start with creating a context. Create a file UserContext.tsx
and write the following code.
import { createContext } from "react";
interface UserContextProps {
{ name: string; surname: string } | undefined
}
const UserContext = createContext<UserContextProps>(undefined);
Here, UserContext
is created with a default value of undefined
. This ensures TypeScript enforces proper checks when consuming the context.
The createContext()
function takes the initial value as a parameter. If there is no default value or it will be fetched dynamically, consider initializing it with undefnied
, as it is easier to catch bugs later.
The createContext()
function returns a context object. One important thing to note here is that the context object doesn't hold any information. It only represents which context components can read or provide. You need to write additional code to provide and consume context values.
Step 2: Create a Provider
Now as we have created the context, we will write a provider function. This function will be wrapped at the root of your project.
In the same file where you created the context, add the following code:
import { useState } from "react";
const UserProvider = ({ children }: { children: React.ReactNode }) => {
const [user, setUser] = useState({ name: "Janardan", surname: "Doekar" });
return (
<UserContext.Provider value={user}>
{children}
</UserContext.Provider>
);
};
export { UserContext, UserProvider };
The UserProvider
wraps child components and provides the user
data to them.
Step 3: Wrap a component with UserProvider
Now, we need to wrap <App />
with UserProvider
.
import { UserProvider } "./context/UserProvider"
import { Parent } from "./components/Parent"
export default function App() {
return (
<UserProvider>
<Parent />
</UserProvider>
)
}
Now all components that are children of the App
component can access values from UserContext directly.
Step 4: Consume the Context in Any Component
import { useContext } from "react";
import { UserContext } from "./UserProvider";
const GrandChild = () => {
const user = useContext(UserContext);
if (user === undefined) return <p>Loading...</p>;
return <p>Hello, {user.name} {user.surname}</p>;
};
Here, useContext(UserContext)
allows GrandChild
to access user
directly, eliminating the need to pass props through intermediate components.
Handling Context Updates
If we want to allow updates to the user
context, we must pass a setter function.
const UserProvider = ({ children }: { children: React.ReactNode }) => {
const [user, setUser] = useState({ name: "Janardan", surname: "Doekar" });
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
Now, any component using useContext(UserContext)
can also update user
.
const GrandChild = () => {
const { user, setUser } = useContext(UserContext);
return (
<div>
<p>{user.name} {user.surname}</p>
<button
onClick={() =>
setUser({ name: "Kaushal", surname: "Joshi" })}
>
Change Name
</button>
</div>
);
};
Using a Custom Hook for useContext
To improve code maintainability and reusability, we can create custom hooks to encapsulate useContext
logic.
import { createContext, useContext, useState } from "react";
interface AuthContextType {
isAuthenticated: boolean;
login: () => void;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | null>(null);
const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const login = () => setIsAuthenticated(true);
const logout = () => setIsAuthenticated(false);
return (
<AuthContext.Provider value={{ isAuthenticated, login, logout }}>
{children}
</AuthContext.Provider>
);
};
// Custom hook for using AuthContext
const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};
const LoginButton = () => {
const { isAuthenticated, login, logout } = useAuth();
return (
<button onClick={isAuthenticated ? logout : login}>
{isAuthenticated ? "Logout" : "Login"}
</button>
);
};
Optimizing Rerenders When Passing Objects And Functions
One common issue when using useContext
is unnecessary rerenders when passing objects or functions as context values. Since objects and functions are reference types in JavaScript, a new reference is created every time the component rerenders, triggering unnecessary updates in consuming components. To resolve this, consider using useMemo()
and useCallback()
to memoize objects and functions to prevent unnecessary rerenders.
import { createContext, useContext, useState, useMemo } from "react";
interface UserContextType {
name: string;
role: string;
}
const UserContext = createContext<UserContextType | null>(null);
const UserProvider = ({ children }: { children: React.ReactNode }) => {
const [user, setUser] = useState({ name: "Janardan", surname: "Doekar" });
const [role, setRole] = useState("Frontend Engineer");
// Memoizing the context value to avoid unnecessary rerenders
const userValue = useMemo(() => ({ user, role }), [user, role]);
const clearUserContext = useCallback(() => {
setUser({ name: "", surname: "" });
setRole("");
}, [])
return (
<UserContext.Provider value={{userValue, clearUserContext}}>
{children}
</UserContext.Provider>
);
};
By using useMemo
and useCallback
, we ensure that the context value doesn't change unnecessarily, reducing re-renders in consuming components.
I recently covered the
useCallback
hook in detail in other article. You can read it here.
Using useContext along with useReducer
useReducer
is often used with useContext
to manage complex state logic. This combination is useful for applications requiring centralized state management without using an external state library like Redux.
Let's see an example where useReducer is used to manage the state and useContext is used to consume it wherever needed.
import { createContext, useReducer, useContext } from "react";
interface CounterContextProps {
{
state: number;
dispatch: React.Dispatch<{ type: string }>
} | null
}
const CounterContext = createContext<CounterContextProps >(null);
const counterReducer = (state: number, action: { type: string }) => {
switch (action.type) {
case "increment":
return state + 1;
case "decrement":
return state - 1;
default:
return state;
}
};
export const CounterProvider = ({ children }: { children: React.ReactNode }) => {
const [state, dispatch] = useReducer(counterReducer, 0);
return (
<CounterContext.Provider value={{ state, dispatch }}>
{children}
</CounterContext.Provider>
);
};
// Custom Hook for using the Counter Context
export const useCounter = () => {
const context = useContext(CounterContext);
if (!context) {
throw new Error("useCounter must be used within a CounterProvider");
}
return context;
};
Now, any component can consume CounterContext
and dispatch actions.
const Counter = () => {
const { state, dispatch } = useCounter();
return (
<di>
<h2>Counter: {state}</h2>
<div>
<button onClick={() => dispatch({ type: "increment" })}>
Increment
</button>
<button onClick={() => dispatch({ type: "decrement" })}>
Decrement
</button>
</div>
</div>
);
};
Using Multiple Contexts In a Single Project
Sometimes, an application requires multiple contexts to manage different concerns, such as authentication, theme, and user preferences.
const App = () => {
return (
<AuthProvider>
<ThemeProvider>
<UserProvider>
<MainComponent />
</UserProvider>
</ThemeProvider>
</AuthProvider>
);
};
export default App;
Each context manages its own slice of state, making it easier to maintain and scale the application.
Using useContext in React 19
In React 19, you can skip the part of providing the context to a parent component. You can directly wrap the component with context. Here's an example:
import { createContext } from "react";
interface UserContextType {
name: string;
surname: string;
}
const UserContext = createContext<UserContextType | null>(null);
export default function App({ children }) {
return (
<UserContext value={ name: "Janardan", surname: "Doekar" }>
{children}
</UserContext>
)
}
And you can consume context just like you used to consume before:
import { useContext } from "react";
import { UserContext } from "./context/UserContext";
export default function GrandChild() {
const user = useContext(UserContext)
if(!user) return <h2>Loading...</h2>
return <h2>Welcome, {user.name} {user.surname}!</h2>
}
Read more about how to use the
useContext
hook in React 19 in official documentation.
useContext with use()
Further in React 19, you can also consume context with use()
. This is useful when you have early returns in your component and need to render Context conditionally.
import { use } from "react";
import { UserContext } from "./context/UserContext";
export default function GrnadChild({ children }) {
if(children === null) {
return null;
}
const user = use(UserContext);
return (
<>
{children}
<p>Welcome, {user.name} {user.surname}!</p>
</>
)
}
As we are returning early if children
is null, the same code would throw an error if we use useContext()
. With the new use()
API, you can call it even after an early return.
When to Use React Context (and When Not To)
React Context is a built-in feature for managing the global state without prop drilling. However, it is not always the best solution for state management. Let's see some scenarios when React Context is appropriate and when other state management approaches should be considered.
When to Use React Context
React Context works best for managing global or shared states that do not change frequently and are used across multiple components.
Theming
If your application supports multiple themes, such as light and dark modes, Context can be used to store and update the theme preference globally.
A
ThemeContext
that stores the current theme and provides a function to toggle between themes.Components can access the theme value without needing to pass props through multiple layers.
Authentication
User authentication status, such as whether a user is logged in or logged out, is a common use case for Context.
A
UserContext
that holds user authentication details and permissions.Components like the navigation bar and profile page can access authentication status without requiring prop drilling.
Language and Internationalization (i18n)
For applications that support multiple languages, Context can store the selected language and provide translations dynamically.
A
LanguageContext
that tracks the selected language (en
,fr
,de
) and updates the UI accordingly.
Globally Shared Configurations
Settings like API base URLs, feature flags, or other global configurations that do not change frequently can be stored in Context.
A
ConfigContext
that stores API endpoints, environment settings (development
,production
), or feature toggles.
When Not to Use React Context
Context is not ideal for frequently changing state or complex data management. Using it in these cases can lead to performance issues.
Frequently Changing State
If a state updates frequently, such as user input fields, animations, or form states, Context is not the best choice. Instead, consider:
useReducer
for managing complex state transitions.State management libraries like Redux, Zustand, or Jotai for better performance.
Context triggers re-renders across all components that consume it, even if only a small part of the state changes. This can lead to unnecessary re-renders and performance degradation.
Data Fetching
Context is not designed for fetching and managing API data. Instead, use:
React Query (TanStack Query) for caching, pagination, and background updates.
Local component state (
useState
) for managing API responses specific to a page or component.Context does not handle caching, automatic retries, or background refetching, which React Query is optimized for.
Interview Questions
What is prop drilling in React? Is it good or bad for the React app? Why?
If prop drilling causes poor performance in React, how can we improve it?
How do you create context in React? How do you provide values and later consume them? Explain with code.
When should you consider using useContext over
useState()
?Explain the differences between React Redux and useContext. When should you use which?
Wrapping Up
I have a love-and-hate relationship with the useContext hook. Despite using it so many times in various projects, I still found myself looking at documentation every time I was writing contexts. Hence, I personally enjoyed writing this article. From now onward, I can write contexts on my own without searching on the web.
I hope you found this article helpful as well. If you did, feel free to share this with your friends and peers. I also wrote some other technical articles that dive deep into various React topics:
If you are looking for a new job or want to share a cool project you built with like-minded folks, check out Peerlist!
All the best for your interviews. Until then, keep grinding.