React
JSX, props, state, reconciliation, hooks, custom hooks.
Volume 7: React Architecture and Engineering
Learning Objectives
- Understand declarative vs imperative rendering and why UI development needed a paradigm shift.
- Master React's unidirectional data flow, component composition, and JSX.
- Internalize the React rendering cycle, reconciliation algorithm, and Fiber architecture.
- Effectively manage component state, side effects, and references using Hooks (useState, useEffect, useRef, useMemo, useCallback).
- Architect scalable client state (Context, Reducers) and understand the boundary between client and server state.
- Implement controlled forms, Error Boundaries, and Suspense for robust user experiences.
Prerequisites
Before proceeding, you must deeply understand:
- DOM Manipulation: How the browser renders HTML, the DOM tree, and imperative DOM APIs.
- JavaScript Closures & Scope: How functions retain access to their lexical scope (vital for Hooks).
- Asynchronous JavaScript: Promises, the Event Loop, and network requests.
- ES6+ Syntax: Destructuring, arrow functions, and spread/rest operators.
Why Does This Exist?
Building complex, interactive User Interfaces (UIs) is inherently difficult because UIs are stateful, long-lived programs. In a traditional request/response model, you render a page and you are done. In a Single Page Application (SPA), the user clicks, types, and drags over minutes or hours. The data (state) changes constantly, and the UI must stay perfectly synchronized with that data.
React was created to solve the State Synchronization Problem. It provides a declarative model where you describe what the UI should look like for a given state, and React handles the tedious, error-prone DOM operations required to get it there.
The Problem Before the Solution
Imagine building a dynamic shopping cart without React. Every time a user adds an item, you must:
// The Imperative Way (Vanilla JS/jQuery)
let cart = [];
function addToCart(item) {
cart.push(item);
// Now we must manually find the DOM elements and update them
const cartList = document.getElementById('cart-list');
const cartTotal = document.getElementById('cart-total');
const badge = document.getElementById('cart-badge');
// Create new element
const li = document.createElement('li');
li.textContent = item.name;
cartList.appendChild(li);
// Update total
let total = cart.reduce((sum, i) => sum + i.price, 0);
cartTotal.textContent = `$${total}`;
// Update badge
badge.textContent = cart.length;
if (cart.length > 0) badge.style.display = 'block';
}
You have to manually track every piece of the DOM that depends on the cart state and write instructions to update it. This is imperative programming—you are telling the computer how to do it, step-by-step.
Why the Old Approach Breaks
The imperative approach fails as applications grow:
- State Desynchronization: What if an item is removed? Or a bulk discount is applied? You have to write specific DOM-updating logic for every possible state transition. Miss one edge case, and the UI shows $0 total while an item is in the list.
- Spaghetti Code: Event listeners manipulate the DOM directly. UI logic becomes tangled with business logic.
- Performance Bottlenecks: Reading from and writing to the DOM simultaneously triggers "layout thrashing" (forced synchronous layouts), severely degrading performance.
History
Around 2011, Facebook was struggling with the "phantom message bug." A user would see a notification badge indicating an unread message, but opening the chat revealed nothing. The state (unread count) and the UI (the badge) had decoupled because the imperative code managing them grew too complex.
Jordan Walke, a Facebook engineer, built FaxJS (the precursor to React) based on functional programming principles. React was open-sourced in 2013. Initially mocked by the community for "putting HTML in JavaScript" (JSX), it soon became the industry standard because it solved the state synchronization problem elegantly.
Mental Model (Analogy -> Reality)
The Analogy: The Stop-Motion Movie
Imagine creating a stop-motion movie. You don't take a frame, explicitly move the puppet's arm 1 inch, adjust the lighting, and snap again. Instead, for every frame, you set up the entire scene exactly how it should look and take a picture.
The Reality: Pure Functions of State
In React, a component is a function. The arguments are data (props/state), and the return value is the UI (HTML). UI = f(State).
When the state changes, React simply calls the function again to get the new UI picture. You don't write code to transition from State A to State B. You just describe State A, and then describe State B. React figures out how to morph the DOM.
Internal Working (The Virtual DOM & Reconciliation)
If React just destroyed the old DOM and recreated it on every data change, it would be catastrophically slow. DOM operations are expensive.
Instead, React uses a Virtual DOM (VDOM). The VDOM is just a lightweight JavaScript object that describes what the real DOM should look like.
- Render Phase: When state changes, React calls your component functions. It generates a new VDOM tree.
- Reconciliation (Diffing): React compares the new VDOM tree with the previous VDOM tree. It calculates the exact differences (the "diff").
- Commit Phase: React applies only those specific differences to the actual browser DOM in one optimized batch operation.
React 16 introduced Fiber, a complete rewrite of the reconciler. Fiber allows React to pause, abort, or reuse work during the render phase. It breaks rendering into chunks, yielding control back to the browser so the main thread isn't blocked by massive UI updates, enabling features like Suspense and Concurrent Mode.
Visual Explanation
State Change triggers Re-render
│
▼
┌───────────────┐ ┌───────────────┐
│ Previous VDOM │ │ New VDOM │
│ <div> │ │ <div> │
│ <h1>0</h1> │ ◄──► │ <h1>1</h1> │ (Diffing Algorithm)
│ </div> │ │ </div> │
└───────────────┘ └───────────────┘
│
▼
(Commit Phase)
Update actual DOM <h1> node
Syntax (JSX and Components)
JSX is a syntax extension for JavaScript. It looks like HTML, but it compiles to JS function calls.
// You write this:
const element = <h1 className="title">Hello</h1>;
// Babel compiles it to this:
const element = React.createElement(
'h1',
{ className: 'title' },
'Hello'
);
A Component is simply a JavaScript function that returns JSX.
Tiny Example
import React, { useState } from 'react';
function Counter({ initialCount }) {
// useState Hook: Returns state variable and a setter function
const [count, setCount] = useState(initialCount);
return (
<div className="counter">
<h2>Count: {count}</h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Walkthrough
Let's trace what happens in the Counter example:
- Initial Mount: React calls
Counter({ initialCount: 0 }).useState(0)initializes the state to 0. The function returns the JSX representing the UI. React commits this to the DOM. - Interaction: The user clicks the button. The arrow function calls
setCount(1). - Re-render: React knows the state changed. It schedules a re-render. It calls
Counteragain. - Second Pass: During this second call,
useState(0)knows the component is already mounted. Instead of returning 0, it returns the current state:1. - Diff & Commit: The function returns JSX with
<h2>Count: 1</h2>. React diffs the old VDOM (Count: 0) with the new VDOM (Count: 1), notices the text node changed, and mutates only that specific text node in the actual DOM.
Break It
Let's break React by violating its core rules.
// BAD: Mutating State Directly
function BadCounter() {
const [count, setCount] = useState(0);
const increment = () => {
count = count + 1; // ❌ React doesn't know about this!
// UI will NOT update.
};
// ...
}
React relies on the setter function (setCount) to know that state has changed and a re-render is required. Mutating the variable directly breaks the unidirectional data flow.
// BAD: Stale Closures in useEffect
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // ❌ `count` is always 0 in this closure
}, 1000);
return () => clearInterval(id);
}, []); // Empty array means this runs once on mount
// ...
}
Because the effect runs only once, the function inside setInterval closes over the initial render's variables, where count is 0. It repeatedly sets the state to 0 + 1. The fix is to use a functional state update: setCount(prev => prev + 1).
Debug It
When React code behaves unexpectedly:
- React Developer Tools: A browser extension that lets you inspect the React component tree, view current props and state, and see which components are re-rendering and why (using the Profiler).
- Strict Mode: Wrapping your app in
<React.StrictMode>intentionally double-invokes render phases and effects in development. If your component is not a pure function (e.g., it accidentally mutates an external variable during render), Strict Mode will make the bug incredibly obvious. - Console Logs inside Render: Placing a
console.log('Rendering MyComponent')right before thereturnstatement helps visualize the re-render cycle.
Mini Project: Controlled Forms and Keys
Let's build a Todo List that demonstrates controlled inputs and the importance of keys.
import { useState } from 'react';
function TodoApp() {
const [text, setText] = useState('');
const [todos, setTodos] = useState([]);
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
// Create a new array to preserve immutability
const newTodo = { id: crypto.randomUUID(), text };
setTodos([...todos, newTodo]);
setText(''); // Reset controlled input
};
return (
<div>
<form onSubmit={handleSubmit}>
{/* Controlled Input: React controls the value, not the DOM */}
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map(todo => (
// Keys help React identify which items changed, were added, or removed.
// NEVER use array index as a key for dynamic lists.
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
Real Application Feature: Data Fetching and Side Effects
React components must be pure. Network requests are "side effects" that belong inside the useEffect hook.
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let ignore = false; // Cleanup flag to prevent race conditions
async function fetchUser() {
setLoading(true);
setError(null);
try {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
if (!ignore) {
setUser(data);
setLoading(false);
}
} catch (err) {
if (!ignore) {
setError(err.message);
setLoading(false);
}
}
}
fetchUser();
// Cleanup function runs before the next effect or on unmount
return () => { ignore = true; };
}, [userId]); // Dependency array: re-run effect ONLY if userId changes
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <div><h1>{user.name}</h1></div>;
}
Production Implementation: Custom Hooks and Context
In production, you don't rewrite fetch logic in every component. You extract it into Custom Hooks to share stateful logic.
// useFetch.js
function useFetch(url) {
const [data, setData] = useState(null);
// ... fetching logic ...
return { data, loading, error };
}
// Global State with Context
// For state that many deeply nested components need (e.g., Theme, Auth),
// prop-drilling becomes painful. The Context API solves this.
const AuthContext = React.createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
return (
<AuthContext.Provider value={{ user, setUser }}>
{children}
</AuthContext.Provider>
);
}
// Consuming Context in any child
function Navbar() {
const { user } = useContext(AuthContext);
return user ? <LogoutButton /> : <LoginButton />;
}
Production Usage (Architecture)
Modern React apps differentiate between two types of state:
- Client State: Ephemeral UI state (is the modal open? what is typed in the search bar?). Handled by
useState,useReducer, or tools like Zustand/Redux. - Server State: Data originating from the database (user profiles, posts). Because server state is asynchronous, cached, and shared, production apps use specialized libraries like React Query (TanStack Query) or SWR instead of standard
useEffectfetches.
Furthermore, standard React is Client-Side Rendered (CSR). The user downloads a blank HTML file and a massive JS bundle, leading to slow Initial Page Loads and poor SEO. In production, React is almost always used within a framework like Next.js or Remix to enable Server-Side Rendering (SSR) or React Server Components (RSC).
Performance
React is fast by default, but complex apps can suffer from excessive re-renders. By default, if a parent component re-renders, all its children re-render, regardless of whether their props changed.
- React.memo: Wrap a child component in
React.memo. It will only re-render if its props have actually changed. - useMemo: Caches the result of an expensive calculation between renders.
- useCallback: Caches a function definition between renders. (Useful when passing callbacks as props to
React.memochildren, so the prop doesn't break equality checks).
Engineering Rule: Do not blindly sprinkle useMemo everywhere. The caching mechanism itself costs memory and CPU. Optimize only when you measure a performance bottleneck.
Best Practices
- Keep Components Pure: Render functions should not have side effects. Given the same props and state, they must return the same JSX.
- Colocate State: Keep state as close to where it is used as possible. Don't put everything in a global store.
- Lift State Up: If two sibling components need to share state, move that state to their nearest common parent.
- Immutability: Never mutate state directly (e.g.,
array.push()). Always return new references (e.g.,[...array, newItem]). React relies on referential equality (oldState === newState) to determine if a render is needed. - Error Boundaries: Wrap critical UI sections in Error Boundaries so a JS error in one component doesn't unmount the entire application.
Interview Questions
Easy: What is the difference between state and props?
Props are data passed into a component from its parent; they are read-only. State is internal data managed by the component itself; it can be updated using the setter function.
Medium: Why do we need the 'key' prop when rendering lists in React?
Keys help React's reconciliation algorithm identify which items have changed, been added, or been removed. Without stable keys, React might destroy and recreate DOM nodes unnecessarily, or worse, mix up component state when the list order changes. Using an array index as a key is dangerous if the list can be reordered.
Hard: Explain the React Fiber architecture and how it improves rendering.
Prior to React 16, reconciliation was a synchronous, recursive process. If the component tree was huge, the main thread was blocked, causing dropped frames (jank). Fiber breaks the work into units (fibers) that represent a component. React can process these fibers in a loop, pausing occasionally (yielding) to let the browser handle high-priority tasks like user input or animations, before resuming the render. This enables concurrent rendering.
Senior: You have a deeply nested component that triggers a re-render of the entire tree on every keystroke in a text input. How do you architect a solution to fix this?
Several approaches: 1) Move the state down. If only the input uses the text state, isolate it in its own component. 2) If the state must live high up, extract the expensive parts of the UI into separate components and wrap them in React.memo, ensuring callbacks passed to them are wrapped in useCallback. 3) Use the children prop pattern. Pass the expensive tree as children to the stateful component. When the state changes, the wrapper re-renders, but React knows the children prop hasn't changed, skipping the render of the expensive tree.
Engineering Challenge
Challenge: Build a custom hook called useDebounce(value, delay). It should take a value (like a search query) and return a debounced version of it that only updates after the specified delay has passed without any new inputs. Use this hook to prevent an API search from firing on every keystroke.
View Solution
import { useState, useEffect } from 'react';
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// Set a timer to update the debounced value
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cleanup: Clear the timeout if the value changes before delay completes
return () => {
clearTimeout(handler);
};
}, [value, delay]); // Only re-run if value or delay changes
return debouncedValue;
}
// Usage Example
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearchTerm) {
// Fire API call here. It will only execute 500ms after the user stops typing.
console.log('Searching API for:', debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}
Revision Sheet
- UI = f(State): React components are pure functions that map state to UI.
- Virtual DOM: An in-memory representation of the DOM. React diffs it to determine minimal DOM updates.
- Hooks:
useState: Local component memory.useEffect: Side effects (network, subscriptions, DOM manipulation outside React).useRef: Mutable value that doesn't trigger a re-render, or direct access to a DOM node.
- Keys: Essential for mapping array items to UI elements stably.
- Immutability: State must be treated as immutable.
Connections
React handles the View layer. But how do you handle routing? How do you render HTML on the server to improve SEO? How do you securely communicate with a database? In the next volume, we graduate from standard React and enter the world of meta-frameworks with Next.js (Volume 8), where we combine React with backend architecture.
Mini Project (20-30 min)
▶ View Solution
Implementation
// code here
Bigger Project (1-2 hours)
Build a comprehensive project for this chapter's topic.
▶ View Solution
Full implementation details
// code here
Interview Questions
Easy: What is a core concept?
A core concept is fundamental to understanding this topic.