The Context API lets data flow through a component tree without manually threading props through every layer in between, making it much simpler to share data across components.
"Prop drilling" is the pattern where data has to be passed from a parent down through several intermediate components just so it can reach a deeply nested child — even though those middle components never use the data themselves. As an app grows, this makes the component tree harder to follow and adds unnecessary boilerplate.
Global state tools — Context API, Redux, Zustand, and similar libraries — exist specifically to solve this.

Context works in three parts:
Start with an empty context and export it so other files can use it.
src/contexts/ThemeContext.js
import { createContext } from "react";
export const ThemeContext = createContext();
Define the state you want to share, then hand it to the provider's value prop while wrapping the components that need access.
App.jsx
import { useState } from "react";
import { ThemeContext } from "./contexts/ThemeContext";
import A from "./A";
import B from "./B";
function App() {
const [text, setText] = useState("");
return (
<ThemeContext.Provider value={{ text, setText }}>
<A />
<B />
</ThemeContext.Provider>
);
}
export default App;
Why two sets of curly braces? The outer {} is standard JSX syntax for embedding a JS expression as a prop value. The inner {} is the actual object literal being passed — in this case { text, setText }. So value={{ text, setText }} means: "embed this JS expression," where the expression happens to itself be an object.
Import the context plus the useContext hook, destructure what you need, and use it freely inside the component.
A.jsx
import React, { useContext } from "react";
import { ThemeContext } from "../contexts/ThemeContext";
const A = () => {
const { text, setText } = useContext(ThemeContext);
return (
<div>
<h1>{text}</h1>
<button onClick={() => setText("Hello, world!")}>Click me</button>
</div>
);
};
export default A;
Rather than managing state in App and exporting a bare context, it's common to bundle the state and provider together into their own component:
src/contexts/ThemeContext.js
import { createContext, useState } from "react";
export const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [text, setText] = useState("");
return (
<ThemeContext.Provider value={{ text, setText }}>
{children}
</ThemeContext.Provider>
);
};
App.jsx
import { ThemeProvider } from "./contexts/ThemeContext";
import A from "./A";
import B from "./B";
function App() {
return (
<ThemeProvider>
<A />
<B />
</ThemeProvider>
);
}
export default App;
This keeps App clean and makes the provider reusable anywhere else it's needed.
Context lets you define shared data once near the top of a tree and read it from any descendant, no matter how deep — cutting out the tedious, error-prone process of manually passing props down through every layer.
Advertisement