Blog Summary: Covers client-side routing for SPAs, solves the prop drilling problem with Context API, and introduces Recoil as a production-grade state management solution.
1. Routing in React
[SOURCE — COURSE MATERIAL]
Jargon First
Single Page Application (SPA):
- Browser downloads ONE HTML file
- React controls what to show based on URL
- No full page reloads when navigating
Client-Side Bundle:
- All your React code compiled into JS files
- Browser downloads once, runs locally
Client-Side Routing:
- URL changes don’t hit the server
- React intercepts and renders appropriate components
Traditional (Multi-Page):
/home → server returns home.html
/about → server returns about.html (full reload)
React SPA (Single-Page):
/home → React shows <Home /> (no reload)
/about → React shows <About /> (no reload, URL changes)
React Router DOM
npm install react-router-dom
import {
BrowserRouter,
Routes,
Route,
Link,
useNavigate,
} from "react-router-dom";
// App.jsx — define routes
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/user/123">User 123</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/user/:id" element={<UserPage />} />
<Route path="*" element={<NotFound />} /> {/* catch-all */}
</Routes>
</BrowserRouter>
);
}
// Accessing route params
import { useParams } from "react-router-dom";
function UserPage() {
const { id } = useParams();
return <h1>User ID: {id}</h1>;
}
// Programmatic navigation
function LoginForm() {
const navigate = useNavigate();
async function handleLogin() {
await login();
navigate("/dashboard"); // redirect after login
}
}
Lazy Loading Routes
import { lazy, Suspense } from "react";
// Load component only when needed (code splitting)
const Dashboard = lazy(() => import("./Dashboard"));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
2. Prop Drilling — The Problem
[SOURCE — COURSE MATERIAL]
Prop drilling = passing props through multiple component layers just to reach a deeply nested component.
// Problem: theme and user must pass through every layer
function App() {
const [user, setUser] = useState({ name: "Alice" });
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <UserProfile user={user} />;
}
function UserProfile({ user }) {
return <h2>{user.name}</h2>; // finally uses it
}
Problems:
- Intermediate components receive props they don’t use
- Refactoring is painful — change in one place ripples everywhere
- Code becomes hard to read
[SOURCE — COURSE MATERIAL]
“Prop drilling doesn’t mean parent re-renders children. It’s the syntactic uneasiness when writing code.”
3. Context API — Teleport State
[SOURCE — COURSE MATERIAL]
Context lets you share state without prop drilling — any component can access it directly.
import { createContext, useContext, useState } from "react";
// 1. Create context
const UserContext = createContext(null);
// 2. Provide it at the top level
function App() {
const [user, setUser] = useState({ name: "Alice" });
return (
<UserContext.Provider value={{ user, setUser }}>
<Layout />
</UserContext.Provider>
);
}
// 3. Consume anywhere in the tree — no prop drilling!
function UserProfile() {
const { user } = useContext(UserContext);
return <h2>{user.name}</h2>;
}
// Intermediate components need NO changes
function Layout() {
return <Sidebar />;
}
function Sidebar() {
return <UserProfile />;
}
Theme Context Example
// theme-context.js
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext("light");
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
// In any component:
function Button() {
const { theme, setTheme } = useTheme();
return (
<button
className={theme}
onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}
>
Toggle Theme
</button>
);
}
Context Limitation
[SOURCE — COURSE MATERIAL]
Context solves prop drilling but does NOT fix unnecessary re-renders.
// When Context value changes, ALL consumers re-render
// even if the part they use didn't change
const ctx = useContext(AppContext);
// If AppContext has { user, posts, theme } and only theme changed,
// components using only user STILL re-render
This is why Recoil (and Redux, Zustand) exist.
4. State Management with Recoil
[SOURCE — COURSE MATERIAL]
Recoil is a state management library that solves Context’s re-render problem.
npm install recoil
Core Concepts
Atom = unit of state (like useState but global)
Selector = derived state (computed from atoms)
Atoms
// atoms.js — define global state
import { atom } from "recoil";
export const networkCountAtom = atom({
key: "networkCount", // unique key
default: 102, // initial value
});
export const notificationCountAtom = atom({
key: "notificationCount",
default: 0,
});
export const jobsCountAtom = atom({
key: "jobsCount",
default: 3,
});
Selectors — Derived State
// Total notification count (sum of atoms)
import { selector } from "recoil";
export const totalNotificationsSelector = selector({
key: "totalNotifications",
get: ({ get }) => {
const network = get(networkCountAtom);
const notifications = get(notificationCountAtom);
const jobs = get(jobsCountAtom);
return network + notifications + jobs;
},
});
Recoil Hooks
import {
useRecoilState, // [value, setter] — like useState
useRecoilValue, // just the value (read-only)
useSetRecoilState, // just the setter (write-only)
} from "recoil";
function NotificationBadge() {
const total = useRecoilValue(totalNotificationsSelector);
return <span>{total}</span>;
}
function NotificationPanel() {
const [count, setCount] = useRecoilState(notificationCountAtom);
return (
<div>
Notifications: {count}
<button onClick={() => setCount((c) => c + 1)}>+</button>
</div>
);
}
// Write-only — useful in handlers that don't need current value
function MarkAllRead() {
const setNotifications = useSetRecoilState(notificationCountAtom);
return <button onClick={() => setNotifications(0)}>Mark all read</button>;
}
RecoilRoot — Wrap Your App
import { RecoilRoot } from "recoil";
function App() {
return (
<RecoilRoot>
<AppBar />
<Main />
</RecoilRoot>
);
}
5. Recoil Advanced — atomFamily & selectorFamily
[SOURCE — COURSE MATERIAL]
The Problem with Multiple Atoms
TODO app: you need one atom per todo
But you don't know how many todos there are upfront
Creating atom1, atom2, atom3... manually doesn't work
atomFamily — Dynamic Atoms
import { atomFamily } from "recoil";
// Creates an atom factory — pass an ID, get an atom
const todoAtomFamily = atomFamily({
key: "todo",
default: (id) => ({
id,
title: "",
completed: false,
}),
});
// In component
function TodoItem({ id }) {
const [todo, setTodo] = useRecoilState(todoAtomFamily(id));
return (
<div>
<p>{todo.title}</p>
<button onClick={() => setTodo({ ...todo, completed: true })}>
Complete
</button>
</div>
);
}
selectorFamily — Dynamic Selectors
import { selectorFamily } from "recoil";
// Fetch todo from server based on ID
const todoSelectorFamily = selectorFamily({
key: "todoFromServer",
get: (id) => async () => {
const res = await fetch(`https://sum-server.100xdevs.com/todo?id=${id}`);
const data = await res.json();
return data.todo;
},
});
// Usage in component
function TodoDetail({ id }) {
const todo = useRecoilValue(todoSelectorFamily(id));
return <div>{todo.title}</div>;
}
6. Recoil Loadable — Handling Async State
[SOURCE — COURSE MATERIAL]
When selectors fetch async data, components need to handle loading/error states.
import { useRecoilValueLoadable } from "recoil";
function TodoDetail({ id }) {
const loadable = useRecoilValueLoadable(todoSelectorFamily(id));
if (loadable.state === "loading") {
return <p>Loading...</p>;
}
if (loadable.state === "hasError") {
return <p>Error: {loadable.contents.message}</p>;
}
// loadable.state === 'hasValue'
const todo = loadable.contents;
return <div>{todo.title}</div>;
}
Asynchronous Selector (Fetches from Backend)
const notificationsSelector = selector({
key: "notifications",
get: async () => {
const res = await fetch("https://sum-server.100xdevs.com/notifications");
const data = await res.json();
return data;
},
});
7. State Management: Decision Tree
[ADDED — EXPLANATION]
Need to share state between components?
│
├── Close in the tree (parent-child or siblings)?
│ └── Use useState + props
│
├── Deeply nested but simple?
│ └── Use Context API
│ (warning: causes all consumers to re-render)
│
└── Large app with frequent state changes?
└── Use Recoil / Zustand / Redux
├── Recoil — atomic, fine-grained re-renders, selector support
├── Zustand — simpler API, less boilerplate
└── Redux — most powerful, highest boilerplate, best devtools
Exercises
Quick (15 min)
Add React Router to a simple app with 3 pages: Home, About, Contact. Add a navigation bar.
Hint 1: Wrap with <BrowserRouter> and use <Link> (not <a>)
Hint 2: 404 page: <Route path="*" element={<NotFound />} />
Intermediate (45 min)
Build a dark/light theme toggle using Context API:
- Theme context with
lightordarkvalue - All components consume it to apply correct styling
- One button anywhere in the app toggles the theme
Challenge (3–4 hours)
Build a LinkedIn-style notification header using Recoil:
- Atoms for: network count, job alerts, messages, notifications
- Selector that sums all for the total badge count
- Each section has +/- buttons to change its own count
- Total badge updates automatically via selector
- Async selector fetches initial values from
https://sum-server.100xdevs.com/notifications
Common Confusions
| Confusion | Reality |
|---|---|
| “Context fixes re-rendering” | Context fixes prop drilling. Re-rendering is still an issue. Use Recoil/Zustand for that. |
| “useRecoilState is like useState for atoms” | Exactly right — same API, but global scope. |
| “Selectors are like useEffect” | No. Selectors derive values; useEffect performs side effects. |
“<Link> and <a> do the same thing” |
<a> causes full page reload. <Link> does client-side navigation. |
Key Takeaways
- React Router: wrap app in
<BrowserRouter>, define<Route>s, use<Link>for navigation - Prop drilling = syntactic pain, not a performance issue
- Context API: creates a teleport for state —
createContext → Provider → useContext - Context limitation: all consumers re-render when value changes
- Recoil: atoms (global state) + selectors (derived state) with fine-grained re-renders
- Use
atomFamily/selectorFamilyfor dynamic collections (like todos with IDs) useRecoilValueLoadablehandles async selectors with loading/error states