What is prop drilling and how does Context solve it?
Show answer — try answering out loud first
Prop drilling is passing a prop through several levels of components that don't use it, just so it reaches one far down the tree. Context lets any descendant read a value directly from a Provider, without going through the intermediate ones.
Imagine the app's visual theme lives at the root and a button five levels down needs it. Without Context, the four intermediate components receive and forward a prop they don't care about. That's prop drilling: it clutters the signatures and makes any change painful.
Context creates a direct "channel":
createContextdefines the channel and its type.- The Provider wraps a tree and publishes the value.
useContext(inside theuseTemahook below) reads it from any descendant.- A custom hook that throws an error outside the Provider prevents using the context without setting it up.
When NOT to use it: Context isn't for all state. It's ideal for global data that rarely changes (theme, language, authenticated user). For local state useState is enough, and since every change to the value re-renders all consumers, don't put state there that changes on every keystroke.
import { createContext, useContext, useState, type ReactNode } from "react";
type Tema = "claro" | "oscuro";
type ContextoTema = { tema: Tema; alternarTema: () => void };
// undefined as the initial value: forces you to use the Provider
const TemaContext = createContext<ContextoTema | undefined>(undefined);
export function ProveedorTema({ children }: { children: ReactNode }) {
const [tema, setTema] = useState<Tema>("claro");
const alternarTema = () =>
setTema((actual) => (actual === "claro" ? "oscuro" : "claro"));
// In React 19 the Context is used directly as the Provider
return <TemaContext value={{ tema, alternarTema }}>{children}</TemaContext>;
}
// Custom hook: fails fast if used outside the Provider
export function useTema(): ContextoTema {
const contexto = useContext(TemaContext);
if (contexto === undefined) {
throw new Error("useTema debe usarse dentro de <ProveedorTema>");
}
return contexto; // here TypeScript already knows it's not undefined
}
// Deep consumer: without passing props through intermediate levels
function BotonTema() {
const { tema, alternarTema } = useTema();
return <button onClick={alternarTema}>Tema actual: {tema}</button>;
}
"Tricking" TypeScript with a fake initial value to avoid dealing with undefined:
// BAD: it compiles, but if you forget the Provider, alternarTema doesn't exist
// and it crashes at runtime with a confusing error
const TemaContext = createContext<ContextoTema>({} as ContextoTema);
The fix is the pattern from the example: undefined as the initial value and a custom hook (useTema) that throws a clear error if there's no Provider. The bug is caught instantly and with a message that says exactly what's missing.
Prop drilling is when I pass a prop through several intermediate components that don't use it, just to reach one far down the tree. Context solves it by creating a direct channel: I wrap the tree with a Provider and any descendant reads the value with
useContext. In TypeScript I type it withundefinedas the initial value and expose a custom hook that throws an error if it's used outside the Provider. That said, I don't use Context for everything: it's for global data that rarely changes, like theme or user, because every change re-renders all consumers.
Create a language context with idioma: "es" | "en" and cambiarIdioma, along with its useIdioma() hook that throws an error outside the Provider.
See solution
import { createContext, useContext, useState, type ReactNode } from "react";
type Idioma = "es" | "en";
type ContextoIdioma = { idioma: Idioma; cambiarIdioma: (nuevo: Idioma) => void };
const IdiomaContext = createContext<ContextoIdioma | undefined>(undefined);
export function ProveedorIdioma({ children }: { children: ReactNode }) {
const [idioma, setIdioma] = useState<Idioma>("es");
return (
<IdiomaContext value={{ idioma, cambiarIdioma: setIdioma }}>
{children}
</IdiomaContext>
);
}
export function useIdioma(): ContextoIdioma {
const contexto = useContext(IdiomaContext);
if (contexto === undefined) {
throw new Error("useIdioma debe usarse dentro de <ProveedorIdioma>");
}
return contexto;
}