DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

React Context & Routing Mastery — From Prop Drilling Pain to Auth‑Ready Architectures

React Context & Routing Mastery — From Prop Drilling Pain to Auth‑Ready Architectures

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 value prop of a Provider?
  • When should I use useContext vs the new use() 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 createBrowserRouter and data APIs change things?
  • Why store search filters in the URL instead of in useState?
  • When should I use useRef instead of useState for 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>;
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

💬 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;
}
Enter fullscreen mode Exit fullscreen mode

💬 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;
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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");
  }
}, []);
Enter fullscreen mode Exit fullscreen mode

💬 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;
}
Enter fullscreen mode Exit fullscreen mode

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"),
  },
]);
Enter fullscreen mode Exit fullscreen mode

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 createBrowserRouter for data flows

State Persistence

  • Hydrate from localStorage or 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)