engineering

How to use the useContext Hook in React?

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

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.

Read more the use() API on React's official documentation.

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

  1. What is prop drilling in React? Is it good or bad for the React app? Why?

  2. If prop drilling causes poor performance in React, how can we improve it?

  3. How do you create context in React? How do you provide values and later consume them? Explain with code.

  4. When should you consider using useContext over useState()?

  5. 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:

  1. What is forwardRef in React?

  2. useRef Hook in React

  3. What Are Portals in React?

  4. What is useLayoutEffect in React?

  5. What is the useCallback hook in React?

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.

Create Profile

or continue with email

By clicking "Create Profile“ you agree to our Code of Conduct, Terms of Service and Privacy Policy.