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 (0)