Previously, we learnt that data flows down via Props.
But what if we have a user object in our top-level <App /> and we need it in a <Avatar /> component deeply nested inside the Navbar?
The Drilling approach:
App → Layout → Header → UserMenu → Avatar
Why is this bad?
Layout and Header don't need user data, but they must accept it as props just to pass it down.Context provides a way to pass data through the component tree without having to pass props down manually at every level. Think of it as Data Teleportation.
We create a store for our data outside of any component.
// AppProvider.tsx
import { createContext } from "react";
// Define the shape of our context
interface AppContextValue {
user: { name: string };
theme: "light" | "dark";
toggleTheme: () => void;
}
// Create the context with null as default
const AppContext = createContext<AppContextValue | null>(null);In production, we wrap the raw provider in a custom component to manage the state logic.
// AppProvider.tsx (Continued)
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const [user, setUser] = useState({ name: "Alice" });
const [theme, setTheme] = useState<"light" | "dark">("light");
const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
};
// ⚠️ CRITICAL: We are bundling unrelated state (User + Theme)
const contextValue = { user, theme, toggleTheme };
return (
<AppContext.Provider value={contextValue}>{children}</AppContext.Provider>
);
};We create a custom hook to access the context easily and safely.
// AppProvider.tsx (Continued)
import { useContext } from "react";
export const useApp = () => {
const context = useContext(AppContext);
if (!context) throw new Error("useApp must be used within AppProvider");
return context;
};This is the biggest pitfall of Context. If we bundle unrelated state (like User and Theme) into a single Context, we force all consumers to re-render when any part of that data changes.
We have two components that consume our useApp hook.
// components/user-info.tsx
const UserInfo = () => {
const { user } = useApp();
// This log helps us track renders
console.log("UserInfo is rendered.");
return <div>Welcome, {user.name}</div>;
};// components/toggle-theme.tsx
const ToggleTheme = () => {
const { theme, toggleTheme } = useApp();
console.log("ToggleTheme is rendered.");
return <Button onClick={toggleTheme}>Current mode: {theme}</Button>;
};What happens when we click the button in <ToggleTheme />?
toggleTheme runs in AppProvider. theme toggles.AppProvider component re-renders.contextValue object is recreated:
// A BRAND NEW object is created in memory
const contextValue = { user, theme, toggleTheme };value prop changed (Old Object !== New Object). It notifies All consumers.> ToggleTheme is rendered.
> UserInfo is rendered. <-- WHY? The user didn't change!Explanation:
Even though UserInfo only destructures { user }, it subscribes to the entire AppContext. Because the AppProvider created a new object for value, UserInfo thinks the data changed and re-renders to stay safe.
In large apps, this causes major performance issues.
| Concept | Definition | Key Role |
|---|---|---|
| Prop Drilling | Passing props through layers that don't need them. | The problem Context solves. |
createContext | Creates a context object. | The tunnel for data. |
<Provider> | Wraps components to give them access. | The broadcaster of data. |
useContext | Hook to access data. | The receiver of data. |
| Split Context | Creating separate Contexts for separate data. | Fixes performance issues. |
Every single component that consumes (useContext) will re-render.
hugeObject.name and we only changed hugeObject.age, Component A will still re-render because the entire value object reference changed.UserContext separate from ThemeContext) or use state management libraries (Redux/Zustand) that allow components to select only the specific data they need.useContext(UserContext) outside of a Provider?It does not crash. It returns the Default Value you passed to createContext(defaultValue).
const UserContext = createContext("Guest");useContext(UserContext) returns "Guest".createContext(null), our code might crash when we try to access user.name (reading property of null).Context is primarily for Prop Drilling (passing data), while Redux is for State Management.
UserInfo is wrapped in React.memo, will it still re-render?Yes. React.memo only checks if Props change. It does not stop re-renders caused by Context changes. If the context value updates, the component re-renders regardless of memo.
We need to Split the Context. instead of one giant AppProvider, we should have:
UserProvider (Holds user state)ThemeProvider (Holds theme state)
By separating them, updating the Theme will only trigger the ThemeProvider, leaving UserInfo (which listens to UserProvider) completely alone.