React Context & Routing Mastery — From Prop Drilling Pain to Auth‑Ready Architectures
frontend · javascript · react · architecture
Most React interviews won’t ask you to build a whole app.
Instead, they test your mental model of Context, routing, state persistence, URL‑driven state, and auth flows with questions like:
- What is prop drilling and how does Context solve it?
- What exactly does a Provider do?
- What goes inside the
valueprop of a Provider? - When should I use
useContextvs the newuse()API? - Why does auth state disappear on full page reload?
- How would you protect routes with a
PrivateRoute? - Why are
<Link>and<Navigate>different? - How does React Router’s
createBrowserRouterand data APIs change things? - Why store search filters in the URL instead of in
useState? - When should I use
useRefinstead ofuseStatefor input values?
This article transforms all those quiz‑style questions into production‑grade patterns you can adopt in real apps — and confidently explain in an interview.
TL;DR — What You’ll Learn
✅ A precise mental model for prop drilling and why Context exists
✅ How Providers actually work and what belongs in their value
✅ When to choose useContext vs the new use() API
✅ How to design an AuthContext that persists sessions
✅ How to build a proper PrivateRoute
✅ How React Router’s data APIs upgrade route definitions
✅ Why URL‑based state beats local state for search filters
✅ When to use useRef to avoid unnecessary rerenders
✅ A full architecture checklist for Context + Routing apps
All examples use TypeScript‑flavored .tsx.
1. Prop Drilling — The Pain Context Was Designed to Fix
Prop drilling happens when data travels through components that do not need it — just to reach a deep child.
Classic example
type User = { name: string };
function App({ user }: { user: User }) {
return <Parent user={user} />;
}
function Parent({ user }: { user: User }) {
return <Child user={user} />;
}
function Child({ user }: { user: User }) {
return <GrandChild user={user} />;
}
function GrandChild({ user }: { user: User }) {
return <p>Hello, {user.name}</p>;
}
Parent and Child become “pipes.”
With Context (the fix)
import { createContext, useContext } from "react";
type User = { name: string };
const UserContext = createContext<User | null>(null);
function App({ user }: { user: User }) {
return (
<UserContext.Provider value={user}>
<SomeTree />
</UserContext.Provider>
);
}
function SomeTree() {
return (
<div>
<GrandChild />
</div>
);
}
function GrandChild() {
const user = useContext(UserContext);
return <p>Hello, {user?.name}</p>;
}
💬 Interview Soundbite
“Prop drilling is a maintainability problem. Context eliminates unnecessary prop chains by letting components subscribe directly to a shared store.”
2. Providers — Architectural Boundaries, Not Just Wrappers
A Provider wraps part of the UI tree and exposes state + actions to descendants.
Example Provider
import { createContext, useContext, useState, ReactNode } from "react";
type Theme = "light" | "dark";
type ThemeContextValue = {
theme: Theme;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>("light");
const value = {
theme,
toggleTheme: () => setTheme(t => (t === "light" ? "dark" : "light")),
};
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within a ThemeProvider");
return ctx;
}
💬 Soundbite
“A Provider is an architectural boundary. The value is the store’s public API.”
3. Designing the value — Your Context’s Public API
Inside the Provider should live:
- State (
useState,useReducer,useRef) - Actions that mutate the state
- Derived values (e.g.,
isAuthenticated)
Example (AuthContext)
type AuthStatus = "checking" | "authenticated" | "not-authenticated";
type AuthContextValue = {
user: { id: string; name: string } | null;
authStatus: AuthStatus;
isAuthenticated: boolean; // derived
login: (token: string) => Promise<void>;
logout: () => void;
};
Why derive values?
- Consumers don’t need to know internal states.
- If you refactor internals, consumers remain stable.
4. useContext vs use() — The New API That Breaks the Rules (Safely)
React 18 introduces use():
const theme = use(ThemeContext);
Key difference:
use() is not a hook, so it:
✔ Can be called inside conditionals
✔ Works in loops
✔ Doesn’t rely on call order
💬 Soundbite
“use() breaks the traditional Rules of Hooks because it isn't a hook. It’s meant for Server Components and Suspense-driven patterns.”
5. Why Auth Context Loses State on Full Page Reload
Because useState lives only in memory.
A reload resets everything.
Fix: Hydrate from storage using an effect
useEffect(() => {
const saved = localStorage.getItem("auth");
if (saved) {
const parsed = JSON.parse(saved);
setUser(parsed.user);
setAuthStatus("authenticated");
} else {
setAuthStatus("not-authenticated");
}
}, []);
💬 Soundbite
“React resets state on reload because it’s ephemeral. Persistent auth requires syncing to storage.”
6. Protecting Routes with PrivateRoute
export function PrivateRoute({ children }: { children: JSX.Element }) {
const { isAuthenticated, authStatus } = useAuth();
if (authStatus === "checking") return <p>Checking session…</p>;
if (!isAuthenticated) return <Navigate to="/login" replace />;
return children;
}
7. <Link> vs <Navigate> — Navigation vs Redirection
| Element | Purpose |
|---|---|
<Link> |
User‑initiated navigation (click) |
<Navigate> |
Immediate redirect during render |
SPA mistake to avoid
❌ <a href="/dashboard">
✔ <Link to="/dashboard">
8. React Router Data APIs (createBrowserRouter)
Modern React Router allows route definitions as JavaScript objects:
export const router = createBrowserRouter([
{
path: "/",
element: <HomePage />,
loader: async () => fetch("/api/home"),
},
]);
Benefits:
- colocated loaders/actions
- better streaming
- supports deferred data
9. URL‑Driven State — Why Filters Should Live in the URL
This connects to your previous knowledge.
Correct principle:
State that should survive refresh or be shareable should be stored in the URL.
Benefits:
- Persistent across page reloads
- Bootstrap initial UI from search params
- Shareable filters (
/search?hero=batman) - Better caching with TanStack Query
💬 Soundbite
“URL is the perfect store for shareable, reload‑safe, bookmarkable state.”
10. useRef vs useState for Input Values
Use useRef when:
- UI doesn’t depend on the input value
- You only need final value (e.g., onSubmit)
- You want to avoid rerenders on every keystroke
11. Architecture Checklist
Context
- Keep Providers high for cross‑cutting concerns
- Expose derived values (
isAuthenticated) - Encapsulate logic inside Provider
Routing
- Always use
<Link>for SPA navigation - Use
<Navigate>in guards - Prefer
createBrowserRouterfor data flows
State Persistence
- Hydrate from
localStorageor cookies - Use URL params for search/filter state
Final Thoughts
React Context, the new use() API, and modern React Router data APIs form a powerful architecture for real‑world UI.
If you understand these patterns — and can explain them with clarity — you’re already at a senior‑level React mindset.
✍️ Written by Cristian Sifuentes
Building resilient front‑ends and teaching teams how to design async UI and modern routing architectures.

Top comments (1)
This is one of the clearest breakdowns I’ve seen on Context + routing patterns. The way you connected prop-drilling pain → Provider architecture → auth persistence → URL-driven state makes the whole mental model click.
Also appreciate the interview-style framing. These are exactly the questions that expose whether someone understands React architecture beyond just syntax. Great read.