By WebNest Studio Engineering Team
React Tutorial
React Context API
As a React application grows, passing data through several layers of components using props becomes tedious and fragile. If a top-level component needs to share a value — a logged-in user, a theme, a language preference — with a deeply nested component, every intermediate component has to accept and forward that prop even if it never uses it itself. This pattern is known as "prop drilling," and the Context API exists specifically to solve it.
Context lets you create a value at one point in the component tree and read it directly from any descendant, no matter how deeply nested, without passing it through every level in between. It is built into React itself, so no extra library is required for simple shared state.
Why it matters
Some data is needed all over an application: the signed-in user, the theme, the language. Passing it through every component in between is tedious and fragile. Context makes a value available to a whole subtree. Used for the right kind of data it removes prop drilling; used for everything it makes the application re-render too much.
Creating and Providing Context
Context is created with createContext(), which returns an object containing a Provider and a Consumer. In modern function-component code you almost always use the Provider component together with the useContext hook, and skip the Consumer render-prop pattern entirely. The Provider accepts a value prop, and every descendant component wrapped inside it can read that value.
const ThemeContext = createContext(defaultValue);— creates the context object.<ThemeContext.Provider value={theme}>— makes "theme" available to all descendants.const theme = useContext(ThemeContext);— reads the nearest matching Provider's value from any descendant.
Avoiding Prop Drilling
Without context, a value needed five levels deep must be passed as a prop through all four intermediate components, even though only the top and bottom components actually care about it. Context removes that obligation: only the component that owns the data renders a Provider, and only the component that needs the data calls useContext. This keeps intermediate components simpler and easier to reuse, because they no longer need to know about data they don't use.
When Context Causes Unnecessary Re-renders
Every component that calls useContext(SomeContext) re-renders whenever the Provider's value prop changes, regardless of which part of that value the component actually uses. A common mistake is passing a brand-new object literal as the value on every render of the parent (e.g. value={{ user, setUser }}), which creates a new reference every time and forces every consumer to re-render even if the relevant data hasn't changed. This is fixed by memoizing the value with useMemo, and by splitting large, frequently-changing state into smaller, more focused contexts instead of one giant "app context."
- Wrap the value object in
useMemo(() => ({ user, setUser }), [user])so it only changes reference when "user" actually changes. - Split contexts by concern (e.g. ThemeContext, AuthContext) instead of one big context, so unrelated updates don't trigger unrelated re-renders.
- For very frequent updates (like mouse position or animation frames), context is usually the wrong tool — consider local state or a dedicated state library instead.
Example
A ThemeContext shared across a component tree without prop drilling
import { createContext, useContext, useMemo, useState } from 'react';
const ThemeContext = createContext(null);
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
};
// Memoize so consumers don't re-render on every ThemeProvider render
const value = useMemo(() => ({ theme, toggleTheme }), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button onClick={toggleTheme}>
Current theme: {theme} (click to toggle)
</button>
);
}
export default function App() {
return (
<ThemeProvider>
<div>
<h1>Context API Demo</h1>
<ThemedButton />
</div>
</ThemeProvider>
);
}
Renders a heading and a button reading "Current theme: light (click to toggle)". Clicking the button flips the theme to "dark" and back, with ThemedButton reading the value directly from context — no props were passed through App.
Common Mistakes
- Passing a new object or array literal directly as the Provider's "value" prop on every render, causing every consumer to re-render unnecessarily — fix with useMemo.
- Calling useContext outside of any matching Provider and being surprised the value is the default (or null), not an error.
- Using one giant context for all global state, which causes unrelated components to re-render whenever any piece of that state changes.
- Reaching for context to solve every prop-passing situation, even when the data is only needed one or two levels down and a simple prop would be clearer.
Exercise
Do this in src/App.jsx of a Vite + React project. Create a ThemeContext with the default value light. In App, provide the value dark to the tree. Toolbar sits in the middle and passes nothing down; ThemedButton, nested inside it, reads the theme from context and uses it as its class name.
Expected result: the button has the class dark, although Toolbar never received or passed a theme prop.
import { createContext, useContext } from "react";
// TODO: create ThemeContext with the default "light"
function ThemedButton() {
// TODO: read the theme from context
return <button>Save</button>;
}
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
export default function App() {
// TODO: provide the value "dark" around Toolbar
return <Toolbar />;
}
Show hints
- Create with
createContext("light")and read withuseContext(ThemeContext). - Provide with
<ThemeContext.Provider value="dark">...</ThemeContext.Provider>.
Show solution
import { createContext, useContext } from "react";
const ThemeContext = createContext("light");
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Save</button>;
}
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
export default function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
Quick quiz
1. Which problem does Context solve?
- A. Slow rendering
- B. Passing a prop through many components that do not use it
- C. Fetching data
- D. Routing between pages
Show answer
B. This is known as prop drilling.
2. When the value of a context changes, which components re-render?
- A. Every component that reads that context
- B. Only the provider
- C. Every component in the application
- D. None
Show answer
A. This is why frequently changing data in one large context can be costly.
3. A component calls useContext(ThemeContext) but has no provider above it. What does it receive?
- A. undefined, always
- B. An error
- C. null
- D. The default value passed to createContext
Show answer
D. The default is used only when there is no matching provider in the tree above.
Interview questions
When should you use Context?
Show answer
For data that many components at different depths need and that changes rarely: the current user, theme, locale, feature flags. It is a way of passing a value down without props, not a state manager in itself; it is normally combined with useState or useReducer in the provider. For data used by only a few nearby components, props are simpler and clearer.
What are the performance pitfalls of Context?
Show answer
Every component that reads a context re-renders when its value changes. Putting unrelated data in one context means a change to any part re-renders all its consumers. Creating the value object inline in the provider gives a new reference on every render, which re-renders consumers even when nothing changed; memoising the value fixes that. Splitting contexts by concern, or using a store library with selectors, avoids broad re-renders.
Key Points to Remember
- createContext + Provider + useContext lets any descendant read shared data without passing it through every intermediate component.
- Context is ideal for rarely-changing, broadly-needed data like theme, authenticated user, or locale.
- Every consumer re-renders when the Provider value reference changes — memoize the value and split contexts by concern to limit that.
- Context is not a full state-management replacement; frequently changing or highly localized state is often better handled elsewhere.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Shows a live preview of plain HTML, CSS and JavaScript. JSX needs a React project on your computer. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering