Skip to content
AstroPaper
Go back

React Core — Complete Deep-Dive Engineering Guide

Updated:
Edit page

React Core — Complete Deep-Dive Engineering Guide

For a frontend engineer moving from “React developer” into senior frontend engineer, React architecture expert, framework-level thinker, and performance-focused engineer.


Table of Contents

Open Table of Contents

1. Big Picture

1.1 What React Actually Is

React is a JavaScript library for building user interfaces through composable, declarative components. It is not a framework — it doesn’t include routing, data fetching, or state management. It does one thing: given state, produce UI. Everything else is the ecosystem’s job.

At its core, React is a reconciler — an engine that compares what your UI should look like (based on current state) with what it currently looks like (in the DOM), and applies the minimal set of changes. You describe the destination; React figures out the journey.

Why this matters: React decoupled what you want from how to get there. Before React, you wrote imperative instructions (“find this DOM node, change its text, add this class”). With React, you write a function that returns what the UI should look like, and React handles the DOM mutations.

1.2 Why React Exists — The Problems It Solves

  1. State-UI synchronization. The hardest problem in frontend: keeping the DOM in sync with application state. Manually tracking which DOM nodes need updating when state changes leads to bugs, especially as UIs grow complex.

  2. Composition. HTML doesn’t compose well. React components are JavaScript functions that return markup — they compose like functions, accept arguments (props), and encapsulate behavior.

  3. Predictability. Given the same state and props, a React component always renders the same output. This makes UIs testable, debuggable, and reasoning about them tractable.

  4. Performance at scale. Direct DOM manipulation is fast for small changes but becomes unmanageable at scale. React’s reconciliation batches and minimizes DOM operations.

1.3 Programming Model Comparison

ApproachMental modelState syncCompositionScaling
Vanilla DOMdocument.querySelector + manual mutationYou track everythingFunctions, but no UI abstractionBreaks quickly
jQuerySelect → mutate → hope it’s consistentManual, error-pronePlugins, no component modelSpaghetti at scale
ReactState → render function → virtual DOM → minimal DOM updatesAutomatic via reconciliationComponents (functions)Designed for large apps
Declarative”Here’s what it should look like”Framework handles syncNatural compositionScales well
Imperative”Here’s the steps to change it”Manual trackingProceduralBreaks at scale

1.4 Core Internal Concepts

Virtual DOM

The virtual DOM is a lightweight JavaScript object tree that mirrors the structure of the real DOM. When state changes, React creates a new virtual DOM tree, diffs it against the previous one, and applies only the necessary changes to the real DOM.

Why not just update the DOM directly? DOM operations are expensive — not because individual operations are slow, but because they trigger layout recalculations, style recomputations, and paints. Batching and minimizing these operations is a significant performance win.

State Change

New Virtual DOM tree (JavaScript objects — cheap)

Diff against previous Virtual DOM tree (JavaScript — cheap)

Minimal DOM mutations (expensive, but minimized)

Browser layout + paint

Important nuance: The virtual DOM is not “faster than the real DOM.” It’s a trade-off: you pay the cost of diffing in JavaScript to avoid paying the cost of unnecessary DOM operations. For trivial UIs, vanilla DOM is faster. For complex, frequently-updating UIs, the virtual DOM wins.

Reconciliation

Reconciliation is React’s diffing algorithm. When you call setState or a component re-renders, React:

  1. Calls your component function to get a new element tree.
  2. Compares it to the previous element tree.
  3. Determines the minimum set of changes needed.
  4. Applies those changes in the commit phase.

Key rules:

Previous tree:          New tree:              Result:
<div>                   <div>                  Update className
  <span>A</span>         <span>A</span>       No change
  <span>B</span>         <span>C</span>       Update text content
</div>                  </div>

Rendering vs. Commit Phase

This is one of the most misunderstood concepts in React.

“Rendering” in React does NOT mean updating the DOM. It means calling your component function.

Phase 1: RENDER (pure, can be interrupted in concurrent mode)
  └── React calls your component function
  └── Produces a new element tree (virtual DOM)
  └── Diffs against previous tree
  └── NO side effects, NO DOM mutations

Phase 2: COMMIT (synchronous, cannot be interrupted)
  └── React applies DOM mutations
  └── Runs useLayoutEffect callbacks
  └── Browser paints
  └── Runs useEffect callbacks

Why this matters: Your component function may be called multiple times without the DOM ever updating. In concurrent mode, React may start rendering, pause, and restart. If your render function has side effects, things break.

Fiber Architecture

Fiber is React’s internal data structure — a linked-list tree of “units of work.” Each fiber node represents a component instance, a DOM element, or a fragment.

FiberNode {
  type: ComponentOrTag,     // 'div', MyComponent, etc.
  stateNode: DOMElement,    // actual DOM node (if host component)
  child: FiberNode,         // first child
  sibling: FiberNode,       // next sibling
  return: FiberNode,        // parent
  pendingProps: {...},
  memoizedState: {...},     // hooks linked list
  effectTag: 'UPDATE',      // what needs to happen in commit
  lanes: 0b0001,            // priority
}

Why Fiber exists: Before Fiber (React 15), reconciliation was recursive and synchronous — once started, it couldn’t be interrupted. A large tree diff would block the main thread, causing dropped frames. Fiber breaks reconciliation into interruptible units of work that can be paused, resumed, and prioritized.

Analogy: Pre-Fiber React was like a single for loop processing 10,000 items — it blocked until done. Fiber is like a generator function (yield) that processes items incrementally, yielding control back to the browser between chunks.

Scheduler

React’s scheduler decides when and in what order to process updates. Not all updates are equal:

PriorityExampleUrgency
Immediate (Sync)Text input typingMust not lag
User-blockingClick handler resultHigh, but can batch
NormalData fetch resultCan defer briefly
LowOff-screen updateCan defer significantly
IdlePrefetch, analyticsWhenever

The scheduler uses the browser’s MessageChannel (not requestIdleCallback) to schedule work in small chunks between frames, keeping the main thread responsive.

Concurrent Rendering

Concurrent rendering means React can work on multiple state updates simultaneously and interrupt lower-priority renders to handle higher-priority ones.

User types in search box (high priority)
  → React starts rendering search results
  → User types another character
  → React ABANDONS the in-progress render
  → Starts a new render with updated input
  → Commits only the final result

Without concurrent rendering:
  → Each keystroke blocks until render completes
  → UI feels sluggish

Opt-in via:

Hydration

Hydration is the process of attaching React’s event handlers and state to server-rendered HTML. The server sends static HTML (fast first paint), then React “hydrates” it — making it interactive without re-creating the DOM.

Server: Render component tree → HTML string → send to client
Client: Parse HTML → display (fast!) → load JS → React hydrates
  → Attaches event listeners
  → Connects state
  → DOM is now interactive

Hydration mismatch: If the server HTML doesn’t match what React would render on the client, React warns and falls back to client-side rendering. This happens when you use Date.now(), Math.random(), or browser-only APIs during render.

1.5 The Full Lifecycle

┌─────────────────────────────────────────────────────────────┐
│ 1. TRIGGER                                                  │
│    setState() / props change / context change / forceUpdate │
└──────────────────────────┬──────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ 2. RENDER PHASE (Pure, interruptible in concurrent mode)    │
│                                                             │
│    React calls component functions top-down                 │
│    Produces new fiber tree (virtual DOM)                    │
│    Diffs against current fiber tree                         │
│    Marks fibers with effect tags (UPDATE, PLACEMENT, etc.)  │
│                                                             │
│    ⚠️ No side effects! No DOM mutations!                    │
└──────────────────────────┬──────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ 3. COMMIT PHASE (Synchronous, cannot be interrupted)        │
│                                                             │
│    a. Before mutation: read DOM (getSnapshotBeforeUpdate)   │
│    b. Mutation: apply DOM changes (insert, update, delete)  │
│    c. Layout: run useLayoutEffect, update refs              │
│                                                             │
└──────────────────────────┬──────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ 4. BROWSER PAINT                                            │
│                                                             │
│    Browser recalculates layout, paints pixels               │
│                                                             │
└──────────────────────────┬──────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ 5. PASSIVE EFFECTS                                          │
│                                                             │
│    useEffect callbacks run asynchronously after paint       │
│    (data fetching, subscriptions, logging)                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

1.6 Comparison With Other Frameworks

DimensionReactVueSvelteSolidJSAngular
ModelVirtual DOM + reconciliationVirtual DOM + reactivityCompile-time, no virtual DOMFine-grained reactivity, no VDOMZone.js + change detection
ReactivityPull-based (re-render whole subtree)Push-based (dependency tracking)Compile-time assignmentsPush-based (signals)Zone.js-based dirty checking
Update granularityComponent-levelComponent-levelStatement-levelDOM node-levelComponent-level
Bundle size~45KB (react + react-dom)~33KB~2-5KB (compiled)~7KB~65KB+
Learning curveMedium (hooks, mental model)Low (options API) / Medium (composition)LowMedium (different paradigm)High (DI, RxJS, modules)
EcosystemMassive, fragmentedLarge, cohesiveGrowingSmallLarge, opinionated
Concurrent renderingYes (unique)No (planned)N/A (no runtime)N/A (fine-grained)No
SSR storyNext.js, RemixNuxtSvelteKitSolidStartAngular Universal
PhilosophyLibrary — bring your own stackProgressive frameworkDisappearing frameworkReactive primitivesFull framework

React’s unique advantages:

React’s weaknesses:

1.7 When React Is a Good Fit

Good fit:

Problematic:


2. Learning Roadmap by Skill Level

Level 1 — Newbie

Goal: Understand JSX, components, props, state, and the basic rendering model.

JSX

JSX is syntactic sugar for React.createElement calls. It looks like HTML but is JavaScript.

// JSX
const element = <h1 className="title">Hello</h1>;

// Compiles to
const element = React.createElement('h1', { className: 'title' }, 'Hello');

// Returns a plain JavaScript object
// { type: 'h1', props: { className: 'title', children: 'Hello' } }

Key rules:

Components

Components are functions that return JSX. They are the unit of composition in React.

// Function component (the only way you should write components in 2026)
function Greeting({ name }: { name: string }) {
  return <h1>Hello, {name}</h1>;
}

// Usage
<Greeting name="Thaison" />;

Mental model: A component is a function. Props are arguments. JSX is the return value. React calls your function, gets back a description of UI, and renders it.

Props

Props are immutable data passed from parent to child. They flow one direction: down.

interface CardProps {
  title: string;
  children: React.ReactNode;
  variant?: 'primary' | 'secondary';
}

function Card({ title, children, variant = 'primary' }: CardProps) {
  return (
    <div className={`card card--${variant}`}>
      <h2>{title}</h2>
      {children}
    </div>
  );
}

Key rule: Never mutate props. They are read-only. If a child needs to change data, it calls a callback prop provided by the parent.

State

State is mutable data owned by a component. When state changes, the component re-renders.

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount((prev) => prev + 1)}>
        Increment (functional update — safer)
      </button>
    </div>
  );
}

Critical mental model: setState does NOT mutate the variable immediately. It schedules a re-render. The new value is available on the next render.

const [count, setCount] = useState(0);

function handleClick() {
  setCount(count + 1);
  console.log(count); // Still 0! State updates are asynchronous.
  setCount(count + 1); // Still uses count=0, so count becomes 1, not 2.
}

// Fix: use functional updates
function handleClickCorrect() {
  setCount((prev) => prev + 1); // prev=0 → 1
  setCount((prev) => prev + 1); // prev=1 → 2
}

Event handling

function Form() {
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // handle form
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} />
      <button type="submit">Submit</button>
    </form>
  );
}

React uses synthetic events — a cross-browser wrapper. Events are pooled and recycled (though this is less relevant since React 17 removed event pooling). Events are delegated to the root, not attached to individual DOM nodes.

Conditional rendering

// Short-circuit
{
  isLoggedIn && <Dashboard />;
}

// Ternary
{
  isLoggedIn ? <Dashboard /> : <LoginForm />;
}

// Early return
function Page({ user }: { user: User | null }) {
  if (!user) return <LoginForm />;
  return <Dashboard user={user} />;
}

Pitfall: {count && <Display />} renders 0 when count is 0 (because 0 is falsy but is a valid React child). Fix: {count > 0 && <Display />}.

List rendering

function TodoList({ items }: { items: Todo[] }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.text}</li>
      ))}
    </ul>
  );
}

Keys are critical. They tell React which items changed, were added, or removed. Without stable keys, React can’t efficiently reconcile lists.

Key approachResult
key={item.id} (stable, unique)✅ Correct — React tracks identity
key={index}⚠️ Breaks on reorder/insert/delete
No key❌ Warning + buggy behavior

Basic hooks

HookPurposeExample
useStateLocal stateconst [x, setX] = useState(0)
useEffectSide effects after renderuseEffect(() => { fetch(...) }, [])
useRefMutable ref that doesn’t trigger re-renderconst ref = useRef(null)

Component lifecycle (hooks mental model)

Mount:    Component function called → DOM inserted → useEffect runs
Update:   State/props change → function called → DOM updated → useEffect cleanup → useEffect runs
Unmount:  useEffect cleanup runs → DOM removed

Common beginner mistakes

MistakeWhy it’s wrongFix
Mutating state directlyReact won’t re-renderCreate new object/array: setItems([...items, newItem])
Using index as key in dynamic listsBreaks identity on reorderUse stable, unique IDs
Side effects in renderUnpredictable, can cause infinite loopsPut side effects in useEffect
{0 && <Component />} rendering 00 is falsy but renderableUse {count > 0 && <Component />}
Forgetting dependency array in useEffectEffect runs every renderAlways specify dependencies
Setting state in a loopCauses multiple re-rendersBatch into one state update

5 beginner exercises

  1. Counter: Build a counter with increment, decrement, and reset buttons.
  2. Toggle: Build a show/hide component that toggles content visibility.
  3. Todo list: Build a list where you can add and remove items. Use proper keys.
  4. Controlled input: Build a search input that filters a list of items as you type.
  5. Fetch and display: Fetch data from a public API on mount, show loading state, display results.

Level 1 success criteria


Level 2 — Junior

Goal: Master hooks, composition patterns, and build interactive UIs with proper data flow.

useEffect

useEffect runs after the browser paints. It’s for side effects: data fetching, subscriptions, DOM measurements, timers.

useEffect(() => {
  // Effect: runs after render
  const controller = new AbortController();

  fetch(`/api/users/${userId}`, { signal: controller.signal })
    .then((res) => res.json())
    .then((data) => setUser(data));

  // Cleanup: runs before next effect or unmount
  return () => controller.abort();
}, [userId]); // Dependency array: re-run when userId changes

Dependency array rules:

PatternBehavior
useEffect(fn)Runs after every render (rarely what you want)
useEffect(fn, [])Runs once after mount (like componentDidMount)
useEffect(fn, [a, b])Runs when a or b changes

Common mistake: Missing dependencies. If your effect uses count but [count] isn’t in the dependency array, the effect captures a stale count.

When NOT to use useEffect:

SituationInstead of useEffectUse
Transform data for renderingEffect + stateCompute during render
Respond to user eventEffect triggered by stateEvent handler directly
Initialize state from propsEffect + setStateuseState(initialValue) or derive
Fetch on mountuseEffectReact Query / useSWR (better error/loading/cache)

React docs: You Might Not Need an Effect

useMemo and useCallback

// useMemo: cache an expensive computation
const sortedItems = useMemo(
  () => items.sort((a, b) => a.name.localeCompare(b.name)),
  [items],
);

// useCallback: cache a function reference
const handleClick = useCallback(
  (id: string) => setSelected(id),
  [], // no dependencies — function never changes
);

When to use:

When NOT to use:

Refs

// DOM ref
function TextInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  const focusInput = () => {
    inputRef.current?.focus();
  };

  return <input ref={inputRef} />;
}

// Mutable value (doesn't trigger re-render)
function Timer() {
  const intervalRef = useRef<number | null>(null);

  useEffect(() => {
    intervalRef.current = window.setInterval(() => {
      // ...
    }, 1000);
    return () => {
      if (intervalRef.current) clearInterval(intervalRef.current);
    };
  }, []);
}

Mental model: useRef is a box that holds a mutable value. Changing .current does NOT cause a re-render. Use it for DOM access, timer IDs, previous values, or any mutable value that shouldn’t trigger re-renders.

Controlled vs. uncontrolled components

TypeState lives inUse case
ControlledReact state (value={state})Validation, formatting, derived state
UncontrolledDOM (ref to access value)Simple forms, file inputs
// Controlled
function ControlledInput() {
  const [value, setValue] = useState('');
  return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}

// Uncontrolled
function UncontrolledInput() {
  const ref = useRef<HTMLInputElement>(null);
  const handleSubmit = () => console.log(ref.current?.value);
  return <input ref={ref} defaultValue="" />;
}

Senior take: Default to controlled. Use uncontrolled only for file inputs or when integrating with non-React libraries.

Context API

// 1. Create context with default value and type
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggle: () => void;
}

const ThemeContext = createContext<ThemeContextType | null>(null);

// 2. Custom hook for type-safe access
function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
  return ctx;
}

// 3. Provider
function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');
  const toggle = useCallback(
    () => setTheme((prev) => (prev === 'light' ? 'dark' : 'light')),
    [],
  );

  const value = useMemo(() => ({ theme, toggle }), [theme, toggle]);

  return (
    <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
  );
}

Context performance pitfall: When context value changes, ALL consumers re-render — even if they only use a part of the value. Solutions:

  1. Split contexts (one for state, one for dispatch).
  2. Memoize the value object with useMemo.
  3. Use external state management (Zustand, Jotai) for frequently-changing state.

Composition patterns

// 1. Children composition (most common)
function Layout({ children }: { children: React.ReactNode }) {
  return <div className="layout">{children}</div>;
}

// 2. Slot pattern
function Dialog({
  header,
  body,
  footer,
}: {
  header: React.ReactNode;
  body: React.ReactNode;
  footer: React.ReactNode;
}) {
  return (
    <div className="dialog">
      <div className="dialog-header">{header}</div>
      <div className="dialog-body">{body}</div>
      <div className="dialog-footer">{footer}</div>
    </div>
  );
}

// 3. Render prop
function DataFetcher<T>({
  url,
  render,
}: {
  url: string;
  render: (data: T) => React.ReactNode;
}) {
  const [data, setData] = useState<T | null>(null);
  useEffect(() => {
    fetch(url)
      .then((r) => r.json())
      .then(setData);
  }, [url]);
  if (!data) return <Spinner />;
  return <>{render(data)}</>;
}

// 4. Compound component
function Select({ children, value, onChange }: SelectProps) {
  return (
    <SelectContext.Provider value={{ value, onChange }}>
      <div className="select">{children}</div>
    </SelectContext.Provider>
  );
}
Select.Option = function Option({ value, children }: OptionProps) {
  const ctx = useSelectContext();
  return <div onClick={() => ctx.onChange(value)}>{children}</div>;
};

Senior preference: Children composition > render props > HOCs. Keep components simple. Use compound components for complex, related UI groups.

Lifting state

When two siblings need shared state, lift it to their common parent.

function Parent() {
  const [selected, setSelected] = useState<string | null>(null);
  return (
    <>
      <Sidebar selected={selected} onSelect={setSelected} />
      <Content selectedId={selected} />
    </>
  );
}

When lifting becomes painful (too many props): Consider context, URL state, or external state management.

Custom hooks

function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue] as const;
}

// Usage
const [theme, setTheme] = useLocalStorage('theme', 'light');

Custom hooks let you extract stateful logic from components — same way you extract pure logic into utility functions. They follow the same rules as hooks (call at top level, only in components/hooks).

Async UI handling

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    setError(null);
    const controller = new AbortController();

    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error('Failed to fetch');
        return res.json();
      })
      .then(setUser)
      .catch((err) => {
        if (err.name !== 'AbortError') setError(err);
      })
      .finally(() => setLoading(false));

    return () => controller.abort();
  }, [userId]);

  if (loading) return <Skeleton />;
  if (error) return <ErrorMessage error={error} />;
  if (!user) return null;
  return <div>{user.name}</div>;
}

Better approach: Use React Query / TanStack Query. It handles loading, error, caching, refetching, and race conditions automatically.

Common anti-patterns

Anti-patternProblemFix
useEffect for derived stateUnnecessary render + effect cycleCompute during render
useEffect to respond to eventsIndirect, hard to traceHandle in event handler
Prop drilling 5+ levelsMaintenance nightmareContext or state management
Giant components (500+ lines)Hard to test, reason aboutExtract sub-components and hooks
any in TypeScriptNo type safetyProper types or unknown
Fetching in useEffect without cleanupRace conditions, memory leaksAbortController or React Query
Sync state from props via useEffectExtra render cycleDerive from props or use key

5 mini project ideas

  1. Multi-step form: Form wizard with validation, back/forward navigation, and state preservation.
  2. Real-time search: Debounced search input that fetches and displays results. Handle loading, error, empty states.
  3. Theme switcher: Dark/light theme with context, persisted to localStorage, respects prefers-color-scheme.
  4. Sortable table: Table with column sorting, pagination, and loading states. Handle 1,000 rows.
  5. Chat UI: WebSocket-based chat with message list, input, typing indicator, and scroll-to-bottom behavior.

Level 2 success criteria


Level 3 — Senior

Goal: Master rendering optimization, React internals, concurrent features, architecture, and production-grade patterns.

Rendering optimization

Rule #1: A component re-renders when:

  1. Its state changes.
  2. Its parent re-renders (even if props are the same).
  3. A context it consumes changes.

Rule #2: Re-renders are NOT inherently bad. React is fast at re-rendering. Only optimize when you measure a real problem.

Optimization toolkit:

ToolWhat it doesWhen to use
React.memoSkips re-render if props haven’t changed (shallow compare)Expensive components with frequent parent renders
useMemoCaches computation resultExpensive calculations
useCallbackCaches function referenceCallbacks passed to React.memo children
children propChildren don’t re-render when parent state changes (if passed as props)Layout components
State colocationMove state down to where it’s usedPrevent unnecessary subtree re-renders
Context splittingSeparate frequently-changing and rarely-changing contextsReduce context consumer re-renders

The “children as props” pattern:

// ❌ Slow: ExpensiveChild re-renders when count changes
function Parent() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>{count}</button>
      <ExpensiveChild />
    </div>
  );
}

// ✅ Fast: ExpensiveChild is created before Parent renders, won't re-render
function App() {
  return (
    <Parent>
      <ExpensiveChild />
    </Parent>
  );
}
function Parent({ children }: { children: React.ReactNode }) {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>{count}</button>
      {children}
    </div>
  );
}

Why this works: children is a prop. The <ExpensiveChild /> element is created in App’s scope, not Parent’s. When Parent re-renders (due to count change), children is the same React element reference — React skips re-rendering it.

Reconciliation deep dive

Reconciliation algorithm (simplified):

1. Compare root elements:
   - Different type? → Unmount old tree, mount new tree.
   - Same type? → Update props, recurse into children.

2. For children:
   - Keyed children: Match by key, reorder efficiently.
   - Unkeyed children: Match by index (fragile!).

3. For each child:
   - Recursively apply step 1.

4. Collect all mutations (effects) into a list.

5. Commit all mutations to DOM synchronously.

What makes this O(n) instead of O(n³):

Concurrent rendering in depth

import { useTransition, useDeferredValue } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // Urgent: update input immediately
    setQuery(e.target.value);

    // Non-urgent: update results in a transition (can be interrupted)
    startTransition(() => {
      setFilteredResults(filterResults(e.target.value));
    });
  };

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending ? <Spinner /> : <Results items={filteredResults} />}
    </div>
  );
}

// Alternative: useDeferredValue
function SearchResults({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query);
  const results = useMemo(() => filterResults(deferredQuery), [deferredQuery]);
  return <ResultList items={results} />;
}

Key insight: useTransition wraps the state setter. useDeferredValue wraps the value consumer. Use useTransition when you control the state update. Use useDeferredValue when you receive the value as a prop.

Suspense

import { Suspense, lazy } from 'react';

// Code splitting with lazy
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <HeavyComponent />
    </Suspense>
  );
}

// Data fetching (with a Suspense-compatible library like React Query v5+)
function UserProfile({ userId }: { userId: string }) {
  const { data: user } = useSuspenseQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

  // No loading check needed — Suspense handles it
  return <div>{user.name}</div>;
}

function App() {
  return (
    <Suspense fallback={<ProfileSkeleton />}>
      <UserProfile userId="123" />
    </Suspense>
  );
}

Mental model: Suspense is a try/catch for loading states. When a child “suspends” (throws a promise), the nearest <Suspense> boundary shows the fallback. When the promise resolves, React retries rendering the child.

Server rendering and hydration

SSR lifecycle:

1. Server: Render component tree → HTML string
   - No useEffect, no browser APIs
   - Data fetched server-side (getServerSideProps, loader, etc.)

2. Network: HTML sent to client
   - User sees content immediately (First Contentful Paint)

3. Client: React hydrates
   - Attaches event listeners
   - Connects state management
   - Page becomes interactive (Time to Interactive)

Streaming SSR (React 18+):
   - Server sends HTML in chunks as components resolve
   - Client hydrates chunks independently
   - Suspense boundaries define chunk boundaries

State architecture

PatternWhen to useExample
Local state (useState)Component-specific, UI stateForm input, toggle, dropdown open
Lifted stateShared between siblingsSelected item in sidebar + content
ContextApp-wide, rarely-changingTheme, locale, auth user
URL stateShareable, bookmarkableSearch params, pagination, filters
External store (Zustand)Frequently-changing, cross-cuttingShopping cart, notifications
Server state (React Query)Remote data with cachingAPI responses

Architecture principle: “State should live as close as possible to where it’s used.”

                    ┌─────────────────────┐
                    │ Server State         │  React Query / SWR
                    │ (API data, cache)    │  Don't duplicate in local state!
                    └────────┬────────────┘

                    ┌────────┴────────────┐
                    │ URL State            │  useSearchParams / router
                    │ (filters, pages)     │  Shareable, bookmarkable
                    └────────┬────────────┘

                    ┌────────┴────────────┐
                    │ Global Client State  │  Zustand / Jotai / Context
                    │ (auth, theme, cart)  │  Rarely, only when needed
                    └────────┬────────────┘

                    ┌────────┴────────────┐
                    │ Local State          │  useState / useReducer
                    │ (UI, forms, toggles) │  Default — keep it here
                    └─────────────────────┘

Testing strategy

LayerToolWhat to test
UnitVitestUtility functions, custom hooks
ComponentTesting LibraryComponent behavior (user interactions, output)
IntegrationTesting Library + MSWFeatures with mocked API
E2EPlaywrightCritical user flows
VisualStorybook + ChromaticComponent appearance, responsive layouts

Philosophy: Test behavior, not implementation. Don’t test that setState was called — test that the UI changed.

// ✅ Good: tests behavior
test('increments counter on click', () => {
  render(<Counter />);
  fireEvent.click(screen.getByRole('button', { name: /increment/i }));
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

// ❌ Bad: tests implementation
test('calls setState', () => {
  const spy = jest.spyOn(React, 'useState');
  // ... testing internal state management
});

Performance profiling

  1. React DevTools Profiler: Record renders, see which components re-rendered and why.
  2. React DevTools “Highlight updates”: Visual indicator of re-renders.
  3. Chrome DevTools Performance tab: Flame chart showing JavaScript execution, layout, and paint.
  4. why-did-you-render: Library that logs unnecessary re-renders.
  5. Lighthouse: Overall performance metrics (LCP, FID, CLS).

Profiling workflow:

  1. Reproduce the slow interaction.
  2. Profile with React DevTools → identify which components re-render unnecessarily.
  3. Profile with Chrome DevTools → identify if the bottleneck is JS, layout, or paint.
  4. Fix the specific bottleneck (memoize, virtualize, debounce, etc.).
  5. Measure again to confirm improvement.

5 production-grade project examples

  1. Dashboard with real-time data: WebSocket feeds, virtualized charts, concurrent rendering for search/filter. State: Zustand for client, React Query for server, URL for filters.
  2. E-commerce product listing: SSR for SEO, infinite scroll, optimistic cart updates, image lazy loading, skeleton loading. Architecture: Next.js App Router + Server Components.
  3. Design system: Component library with Storybook, compound components, accessible primitives, theme system via CSS variables + context.
  4. Multi-step onboarding wizard: URL-based step tracking, form validation with Zod, state preservation across steps, analytics tracking.
  5. Content management editor: Rich text editing, autosave with debounce, optimistic updates, conflict resolution, undo/redo with useReducer.

Level 3 success criteria


Level 4 — Expert

Goal: Understand React at the framework level — fiber, scheduler, compiler, Server Components, and architectural philosophy.

Fiber architecture deep dive

Every React element maps to a fiber node. The fiber tree is a mutable work-in-progress structure that React traverses during reconciliation.

Fiber tree traversal (DFS):

    App (fiber)

    ├── child → Header (fiber)
    │            │
    │            └── child → Logo (fiber)
    │                         │
    │                         └── sibling → Nav (fiber)

    └── sibling → Main (fiber)

                   └── child → Content (fiber)

Traversal order: App → Header → Logo → Nav → Main → Content

Each fiber has:
  .child     → first child
  .sibling   → next sibling
  .return    → parent

Double buffering: React maintains two fiber trees:

On commit, they swap. This allows React to prepare the next UI without touching the DOM until everything is ready.

React Scheduler mental model

Update queue:
  ┌─────────────────────────────────────────┐
  │ Sync (discrete events: click, input)    │ ← highest priority
  │ Continuous (drag, scroll)               │
  │ Default (setState in setTimeout)        │
  │ Transition (startTransition)            │
  │ Idle (offscreen, prefetch)              │ ← lowest priority
  └─────────────────────────────────────────┘

Scheduler loop:
  1. Pick highest priority work
  2. Process fiber nodes (render phase)
  3. Check: has 5ms elapsed?
     - Yes → yield to browser (MessageChannel) → resume later
     - No → continue processing
  4. When all fibers processed → commit to DOM

Rendering lanes: React uses a bitmask system to track update priorities. Each update is assigned a “lane” (a bit). Multiple updates can share a lane, and React processes lanes from highest to lowest priority.

SyncLane:        0b0000000000000000000000000000001
InputContinuousLane: 0b0000000000000000000000000000100
DefaultLane:     0b0000000000000000000000000010000
TransitionLane1: 0b0000000000000000000000001000000
IdleLane:        0b0100000000000000000000000000000

React Compiler (React Forget)

The React Compiler automatically memoizes components and hooks — removing the need for manual useMemo, useCallback, and React.memo.

What it does:

What it means for you:

Current status: Production at Meta, available as a Babel plugin. Being integrated into frameworks.

React Compiler docs

React Server Components (RSC)

RSC splits components into two types:

TypeRuns onCan useBundle impact
Server Component (default in App Router)Server onlyasync/await, DB, file systemZero JS sent to client
Client Component ('use client')Server (SSR) + ClientuseState, useEffect, eventsJS sent to client
// Server Component (default) — runs on server, zero client JS
async function UserList() {
  const users = await db.query('SELECT * FROM users');
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          <UserCard user={user} /> {/* Server Component */}
          <LikeButton userId={user.id} /> {/* Client Component */}
        </li>
      ))}
    </ul>
  );
}

// Client Component — has interactivity
('use client');
function LikeButton({ userId }: { userId: string }) {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(!liked)}>❤️</button>;
}

Mental model: Server Components are the default. You opt into the client with 'use client'. Think of 'use client' as a boundary — everything below it becomes client code.

Why this matters:

Streaming SSR

Traditional SSR:
  Server renders ALL components → sends complete HTML → client hydrates ALL

Streaming SSR (React 18+):
  Server renders what it can → sends partial HTML → streams remaining as Suspense boundaries resolve
  Client hydrates each chunk independently

  Browser receives:
  1. <html><body><Header /><Sidebar />  ← immediate
  2. <Suspense fallback> loading...     ← placeholder
  3. <!-- stream: resolved content -->  ← arrives when data is ready

Benefits: Faster Time to First Byte, progressive rendering, better perceived performance.

Framework comparison philosophy

PhilosophyReactVueSvelteSolidJS
Core beliefUI = f(state), composition over conventionProgressive enhancement, approachableLess code, compile-timeFine-grained reactivity
Trade-offFreedom + ecosystem vs. decision fatigueCohesion vs. smaller ecosystemSmall bundles vs. less flexibilityPerformance vs. smaller community
RenderingCoarse (component-level), optimize with memoCoarse (component-level), auto-trackingFine (surgical DOM updates)Fine (signal-based)
Future betCompiler will solve memoization; RSC for server-firstVapor mode (compile-time)Already compiledAlready fine-grained

Expert insight: React’s “re-render everything” model is a deliberate trade-off. It’s simple to reason about (predictable) at the cost of performance (addressed by memoization and the compiler). Solid’s fine-grained model is more performant but harder to compose and debug. React bets that a compiler can bridge the gap.

Architecture review checklist

What expert engineers care about that juniors miss

Expert concernJunior blind spot
Render count under real conditions”It works”
Bundle size per route”We’ll optimize later”
Hydration mismatch in SSR”Works in dev”
State ownership and boundaries”Just use global state”
Accessibility from day one”We’ll add it later”
Error boundaries for resilience”It won’t crash”
Component API design (props surface area)“Just add another prop”
Key stability in dynamic lists”Index works fine”
Effect cleanup and race conditions”It fetches data”
Concurrent rendering compatibility”We don’t use transitions”
Server/client component boundary”Everything is a client component”
Testing behavior, not implementation”Mock everything”

10 advanced engineering discussion topics

  1. Signals vs. VDOM: Will React adopt signals (like Solid, Angular, Preact)? What would it sacrifice? What would it gain?
  2. Compiler limits: What patterns can’t the React Compiler optimize? How does this constrain component design?
  3. RSC data flow: In a Server Components world, is React Query still necessary? How does caching work across the server/client boundary?
  4. Hydration cost: For a large e-commerce page, hydration can take 2-5 seconds. What strategies exist to reduce this (partial hydration, islands, progressive hydration)?
  5. Component boundaries: When should you split a component? When does splitting harm performance (more fibers, more reconciliation)?
  6. State machine UI: Should complex UI flows (multi-step forms, wizards, modals) be modeled as state machines (XState)? What’s the trade-off vs. useState/useReducer?
  7. Micro-frontends with React: When does it make sense? How do you share state, routing, and styles across independent React apps?
  8. React Native convergence: Should web and mobile share component logic? At what level of abstraction?
  9. Build-time vs. runtime: React leans runtime (VDOM, hooks). Svelte leans build-time. Where is the optimal balance? How does the React Compiler shift this?
  10. Framework lock-in: If you build a 200-component design system in React, what’s the migration cost to another framework? How do you minimize it?

3. Setup Guide

Step 1: Project setup

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
npx create-next-app@latest my-app --typescript --tailwind --eslint --app --src-dir
cd my-app
npm run dev

Astro (for content-heavy sites with React islands)

npm create astro@latest my-site
cd my-site
npx astro add react
npm run dev

Step 2: TypeScript configuration

// tsconfig.json (key settings)
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true, // arrays[i] returns T | undefined
    "exactOptionalPropertyTypes": true, // distinguishes undefined from missing
    "jsx": "react-jsx",
    "moduleResolution": "bundler",
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "paths": {
      "@/*": ["./src/*"],
    },
  },
}

Step 3: ESLint setup

npm install -D eslint @eslint/js typescript-eslint eslint-plugin-react-hooks eslint-plugin-react-refresh

Essential rules:

Step 4: Folder structures

Small project

src/
├── components/
│   ├── Button.tsx
│   ├── Card.tsx
│   └── Header.tsx
├── hooks/
│   └── useLocalStorage.ts
├── pages/           (or routes/)
│   ├── Home.tsx
│   └── About.tsx
├── utils/
│   └── formatDate.ts
├── App.tsx
└── main.tsx

Medium project

src/
├── components/       ← shared/generic UI components
│   ├── ui/
│   │   ├── Button.tsx
│   │   ├── Input.tsx
│   │   └── Modal.tsx
│   └── layout/
│       ├── Header.tsx
│       ├── Footer.tsx
│       └── Sidebar.tsx
├── features/         ← domain-specific modules
│   ├── auth/
│   │   ├── components/
│   │   ├── hooks/
│   │   ├── api.ts
│   │   └── types.ts
│   ├── dashboard/
│   │   ├── components/
│   │   ├── hooks/
│   │   └── types.ts
│   └── settings/
├── hooks/            ← shared custom hooks
├── lib/              ← third-party wrappers, config
├── stores/           ← global state (Zustand)
├── types/            ← shared types
├── utils/            ← pure utility functions
├── App.tsx
└── main.tsx

Large enterprise project

src/
├── app/              ← Next.js App Router / routing
│   ├── (auth)/
│   ├── (dashboard)/
│   └── layout.tsx
├── components/       ← shared design system
│   ├── primitives/   ← atomic components (Button, Input)
│   ├── patterns/     ← composed components (SearchBar, DataTable)
│   └── layouts/      ← page layouts
├── features/         ← domain modules (each is self-contained)
│   ├── billing/
│   │   ├── components/
│   │   ├── hooks/
│   │   ├── api/
│   │   ├── stores/
│   │   ├── types.ts
│   │   └── index.ts   ← public API of the feature
│   └── users/
├── hooks/            ← truly shared hooks
├── lib/              ← framework wrappers
│   ├── api-client.ts
│   ├── query-client.ts
│   └── auth.ts
├── stores/           ← global stores
├── types/            ← shared types / API schemas
└── utils/            ← pure functions

Principle: Features are self-contained modules. They can import from components/, hooks/, lib/, utils/, types/. They must NOT import from other features (use events, stores, or API to communicate).

Step 5: State management integration

import { create } from 'zustand';

interface CartStore {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  total: () => number;
}

const useCartStore = create<CartStore>((set, get) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
  removeItem: (id) =>
    set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
  total: () => get().items.reduce((sum, item) => sum + item.price, 0),
}));
import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      gcTime: 10 * 60 * 1000, // 10 minutes
      retry: 1,
      refetchOnWindowFocus: false,
    },
  },
});

function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then((r) => r.json()),
  });
}

Step 6: Testing setup

npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom msw
// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    globals: true,
  },
});

// src/test/setup.ts
import '@testing-library/jest-dom/vitest';

Step 7: Debugging tooling

ToolPurpose
React DevToolsComponent tree, props, state, profiler
React DevTools ProfilerRender timing, commit chart
Chrome DevTools PerformanceJS execution, layout, paint
why-did-you-renderLog unnecessary re-renders
React strict modeDetect impure renders, missing cleanups

4. Cheatsheet

Hooks reference

HookSignaturePurpose
useStateconst [val, set] = useState(init)Local state
useReducerconst [state, dispatch] = useReducer(reducer, init)Complex state logic
useEffectuseEffect(() => { ... return cleanup }, [deps])Side effects after paint
useLayoutEffectSame as useEffectSide effects before paint (DOM measurements)
useRefconst ref = useRef(init)Mutable value / DOM ref
useMemoconst val = useMemo(() => compute, [deps])Cache computation
useCallbackconst fn = useCallback(fn, [deps])Cache function reference
useContextconst val = useContext(MyContext)Read context value
useIdconst id = useId()Unique ID for accessibility
useTransitionconst [pending, start] = useTransition()Non-urgent state updates
useDeferredValueconst deferred = useDeferredValue(val)Defer a value
useImperativeHandleuseImperativeHandle(ref, () => api)Customize ref exposed to parent
useSyncExternalStoreuseSyncExternalStore(sub, getSnap)Subscribe to external store

Rendering rules

RuleDetails
Components re-render when state changessetState triggers re-render of component + children
Parent re-render → children re-renderUnless children are memoized (React.memo)
Context change → all consumers re-renderEven if they use only part of the value
Props change alone doesn’t cause re-renderParent re-rendering does (props change is a consequence)
setState with same value → no re-renderReact bails out (but the function may still be called once)
Render must be pureNo side effects, no DOM mutation, no subscriptions

State update rules

// Batching: multiple setStates = one re-render (React 18+)
function handleClick() {
  setCount((c) => c + 1);
  setFlag((f) => !f);
  // One re-render, not two
}

// Object state: always create new object
setUser({ ...user, name: 'New' }); // ✅
user.name = 'New';
setUser(user); // ❌ Same reference, no re-render

// Array state: always create new array
setItems([...items, newItem]); // ✅ Add
setItems(items.filter((i) => i.id !== id)); // ✅ Remove
setItems(items.map((i) => (i.id === id ? { ...i, done: true } : i))); // ✅ Update
items.push(newItem);
setItems(items); // ❌ Mutates original

useEffect dependency rules

DependencyEffect runs
[a, b]When a or b changes (Object.is comparison)
[]Once after mount
omittedEvery render (almost never correct)

Object/array dependencies: Objects and arrays are compared by reference. { a: 1 } !== { a: 1 }. Solutions:

  1. Destructure: [obj.a, obj.b] instead of [obj].
  2. Stabilize with useMemo.
  3. Use a primitive derived value.

Common composition patterns

// Compound component
<Select value={v} onChange={setV}>
  <Select.Option value="a">A</Select.Option>
  <Select.Option value="b">B</Select.Option>
</Select>

// Render prop
<Tooltip render={({ isVisible }) => isVisible ? <Info /> : null} />

// Polymorphic component
<Button as="a" href="/about">Link Button</Button>

// Forwarded ref
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
  <input ref={ref} {...props} />
));

Common React warnings

WarningCauseFix
”Each child in a list should have a unique key”Missing or non-unique key propAdd stable key={item.id}
”Cannot update a component while rendering another”Calling setState of parent during child renderMove to useEffect or event handler
”Maximum update depth exceeded”Infinite re-render loopFix useEffect deps or conditional setState
”Hydration mismatch”Server/client render different outputRemove Date.now(), Math.random() from render
”Can’t perform a React state update on an unmounted component”setState in async callback after unmountUse AbortController or check mounted ref
”React Hook useEffect has a missing dependency”Effect uses a value not in deps arrayAdd it to deps or restructure

TypeScript patterns

// Component props
interface ButtonProps {
  variant: 'primary' | 'secondary';
  size?: 'sm' | 'md' | 'lg';
  children: React.ReactNode;
  onClick?: () => void;
}

// Extending HTML element props
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
  error?: string;
}

// Generic component
function List<T>({
  items,
  renderItem,
}: {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}) {
  return (
    <ul>
      {items.map((item, i) => (
        <li key={i}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Discriminated union for component states
type AsyncState<T> =
  | { status: 'loading' }
  | { status: 'error'; error: Error }
  | { status: 'success'; data: T };

// Event handlers
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {};

Performance tips

TipWhen
Colocate stateAlways — move state down to where it’s used
React.memoWhen profiling shows unnecessary re-renders of expensive components
useMemoExpensive computations (sorting, filtering large arrays)
useCallbackCallbacks passed to React.memo children
Virtualize long lists> 100 items visible, use @tanstack/react-virtual
Lazy load routesAlways — React.lazy + Suspense for route-level code splitting
Debounce search inputUser-typed input triggering expensive operations
Avoid inline objects in JSXstyle={{ color: 'red' }} creates new object every render

Accessibility tips

PracticeHow
Semantic HTMLUse <button>, <nav>, <main>, <article>
ARIA labelsaria-label, aria-labelledby, aria-describedby
Keyboard navigationonKeyDown, tabIndex, focus management
Focus managementuseRef + .focus() for modals, dialogs
Live regionsaria-live="polite" for dynamic content
Color contrast4.5:1 minimum for text
useId for form labelsUnique IDs for htmlFor / id pairs
Skip navigation linkHidden link to skip to main content

5. Real-World Engineering Mindset

Form management

Problem: Forms need validation, error display, submission handling, and state management. Simple forms are easy; complex forms (multi-step, dynamic fields, async validation) are hard.

StrategyComplexity it handlesBundle sizeDX
useState + manualSimple forms0KBTedious at scale
useReducer + manualMedium forms0KBMore structured
React Hook FormComplex forms~9KBExcellent
FormikComplex forms~13KBGood but heavier
Conform (for RSC)Server-validated forms~5KBNext.js native

Senior choice: React Hook Form + Zod for validation. Uncontrolled by default (performance), with register for simple inputs and Controller for custom components.

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: zodResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}
    </form>
  );
}

Global state

Problem: Some state needs to be accessed across many components (auth user, theme, shopping cart).

StrategyRe-render scopeDevToolsPersistenceBest for
ContextAll consumersReact DevToolsManualRarely-changing (theme, locale)
ZustandSelector-based (minimal)Zustand DevToolsMiddlewareClient UI state
JotaiAtom-based (minimal)Jotai DevToolsAtom-levelFine-grained shared atoms
Redux ToolkitSelector-basedRedux DevToolsRTK middlewareLarge teams wanting convention
React QueryQuery-basedRQ DevToolsBuilt-in cacheServer state (NOT global client state)

Senior choice: Zustand for client state (small API, minimal re-renders). React Query for server state. Context only for theme/locale/auth. Never Redux for a new project unless the team already knows it.


Table rendering

Problem: Tables with sorting, filtering, pagination, and many rows are performance-sensitive.

StrategyRow limitFeaturesBundle
Plain <table>< 100 rowsManual sorting/filtering0KB
TanStack Table (headless)< 10,000 rowsFull-featured, bring your own UI~15KB
Virtualized table (TanStack Virtual)100,000+ rowsOnly renders visible rows~5KB
AG GridUnlimitedEnterprise features200KB+

Senior choice: TanStack Table for most cases. Add virtualization (@tanstack/react-virtual) when rows exceed ~500. AG Grid only for spreadsheet-like requirements.


Virtualization

Problem: Rendering 10,000+ DOM nodes causes jank.

Solution: Only render items visible in the viewport + a small overscan buffer.

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }: { items: Item[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
    overscan: 5,
  });

  return (
    <div ref={parentRef} style={{ height: 400, overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map((row) => (
          <div
            key={row.key}
            style={{
              position: 'absolute',
              top: row.start,
              height: row.size,
            }}
          >
            {items[row.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}

SSR vs. CSR vs. SSG

RenderingWhen HTML is generatedSEOTTFBInteractivityBest for
CSRClient (after JS loads)PoorFast (empty shell)After JSDashboards, internal tools
SSRServer (per request)ExcellentSlowerAfter hydrationDynamic pages with SEO
SSGBuild timeExcellentFastestAfter hydrationMarketing, blogs, docs
ISRBuild + revalidationExcellentFastAfter hydrationE-commerce, content sites
RSCServer (streamed)ExcellentFast (streaming)ProgressiveNext.js App Router apps

Senior choice: Default to SSR/RSC (Next.js App Router). Use SSG for truly static content. Use CSR only for authenticated dashboards. ISR for content that changes hourly/daily.


Code splitting

import { lazy, Suspense } from 'react';

// Route-level splitting (always do this)
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

// Component-level splitting (for heavy components)
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));

function App() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

Senior choice: Always split at route level. Split heavy components (editors, charts, maps) at component level. Monitor bundle sizes with source-map-explorer or @next/bundle-analyzer.


Optimistic updates

Problem: Waiting for server response before updating UI feels slow.

const mutation = useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    await queryClient.cancelQueries({ queryKey: ['todos'] });
    const previous = queryClient.getQueryData(['todos']);
    queryClient.setQueryData(['todos'], (old) =>
      old.map((t) => (t.id === newTodo.id ? { ...t, ...newTodo } : t)),
    );
    return { previous };
  },
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context.previous); // Rollback
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] }); // Refetch truth
  },
});

Senior choice: Use React Query’s optimistic update pattern. Always implement rollback. Show subtle indicators that the change is pending.


Problem: Modals need to render outside the component tree (portals), manage focus, handle escape key, and prevent body scroll.

Senior choice: Build on top of Radix UI or headless UI primitives. Use portals (createPortal). Manage focus trap. Handle Escape key. Prevent background scroll. Use compound component pattern for composability.


Design systems

Senior choice: Build on Radix UI primitives (accessible, unstyled). Style with Tailwind CSS or CSS Modules. Publish as a shared package in a monorepo. Document with Storybook. Test visually with Chromatic.


6. Brainstorm / Open Questions

Architecture

  1. Should this state live in the component, in context, in a store, or in the URL?
  2. When does a component become “too big”? What are the signals?
  3. How do you decide between a feature folder structure and a type-based folder structure?
  4. Should Server Components replace API routes for data fetching?
  5. When should you create a custom hook vs. just write the logic inline?
  6. How do you enforce feature module boundaries in a large codebase?
  7. When does component composition break down and you need a state machine?
  8. Should your design system be a separate package or a folder in the monorepo?

Rendering

  1. Why does React re-render the entire subtree instead of just the changed component?
  2. What breaks if concurrent rendering interrupts a render that reads from a mutable source?
  3. When should you use useLayoutEffect instead of useEffect?
  4. What happens when a Suspense boundary is nested inside another Suspense boundary?
  5. How does React decide which updates to batch?
  6. What is the cost of having 500 useMemo calls in a component tree?
  7. Why does React call your component function twice in Strict Mode?
  8. What happens when key changes on a component — does it re-render or remount?

Performance

  1. How do you identify whether a performance problem is in React (JS) or in the browser (layout/paint)?
  2. When is React.memo harmful (costs more than it saves)?
  3. How would this component behave with 10,000 items?
  4. What’s the performance difference between CSS-in-JS and CSS Modules in SSR?
  5. How much does hydration cost for a page with 200 interactive components?
  6. When should you move computation to a Web Worker?
  7. What’s the impact of 50 context providers nested at the root?
  8. How do you measure and budget JavaScript bundle size per route?

DX / Maintainability

  1. How do you prevent prop drilling without reaching for global state?
  2. When is TypeScript strict mode worth the extra effort?
  3. How do you write tests that don’t break when implementation changes?
  4. Should you co-locate tests next to components or in a separate folder?
  5. How do you manage 100+ components without a design system?
  6. When does abstraction hurt more than it helps?
  7. How do you onboard a new engineer to a 500-component codebase?
  8. Should stories in Storybook be the single source of truth for component documentation?

Accessibility

  1. How do you ensure keyboard navigation works in a custom dropdown?
  2. What ARIA attributes does a modal need to be accessible?
  3. How should focus behave when a route changes in a SPA?
  4. When should you use aria-live vs. focus management for dynamic content?
  5. How do you test accessibility automatically and what can’t be automated?
  6. What’s the cost of adding accessibility retroactively vs. from day one?

Scaling

  1. At what team size does a monorepo become essential?
  2. How do you prevent one team’s changes from breaking another team’s components?
  3. When should you migrate from a single Next.js app to micro-frontends?
  4. How do you share components between React web and React Native?
  5. What deployment strategy works for 50 engineers shipping to the same app?
  6. How do you manage CSS at scale without conflicts?

Product trade-offs

  1. When should you ship with known performance issues vs. fix them first?
  2. What’s the cost of choosing the wrong state management library?
  3. Should you build a custom component or use a library (trade-offs of each)?
  4. When is “good enough” UI better than “perfect” UI?
  5. How do you balance developer experience with user experience?
  6. When should you rewrite a component from scratch vs. refactor incrementally?
  7. What’s the real cost of adding a new npm dependency?
  8. Should you optimize for first render performance or interaction performance?

7. Practice Questions

Beginner (15 questions)

Q1. True/False: JSX is HTML.

AnswerFalse. JSX is syntactic sugar for React.createElement calls. It looks like HTML but is JavaScript. Key differences: className not class, htmlFor not for, camelCase attributes, self-closing tags required.

Q2. Single choice: What does useState return?

AnswerC. useState returns a tuple: [currentValue, setterFunction]. Array destructuring lets you name them anything.

Q3. Fill in the blank: To prevent a list rendering bug, every item in a .map() must have a unique _______ prop.

Answerkey. Keys give React stable identity for each item. Without stable keys, React can't efficiently reconcile additions, removals, or reorders.

Q4. Debugging: This code renders 0 instead of nothing when count is 0. Why?

{
  count && <Badge count={count} />;
}
Answer0 is falsy in JavaScript, so the short-circuit evaluates to 0. But 0 is a valid React child and renders as text. Fix: {count > 0 && <Badge count={count} />}.

Q5. Single choice: When does a component re-render?

AnswerC. A component re-renders when its own state changes, when its parent re-renders (passing new or same props), or when a consumed context value changes.

Q6. True/False: setState updates the value immediately.

AnswerFalse. setState schedules a re-render. The new value is available on the next render, not immediately after calling setState.

Q7. Scenario: You have this code. What renders?

function App() {
  return <Greeting name="React" />;
}
function Greeting({ name }: { name: string }) {
  return <h1>Hello, {name}!</h1>;
}
Answer<h1>Hello, React!</h1>. The name prop is passed from App to Greeting and interpolated into JSX.

Q8. Multiple choice: Which are valid ways to handle events in React? (Select all)

AnswerA and C. React uses camelCase event names (onClick, not onclick). D works but bypasses React's event system and won't be cleaned up automatically.

Q9. Fill in the blank: Components must start with a ___ letter.

AnswerCapital (uppercase). <myComponent /> is treated as an HTML tag. <MyComponent /> is treated as a React component.

Q10. Debugging: Why does this component not update when the button is clicked?

function Counter() {
  let count = 0;
  return (
    <button
      onClick={() => {
        count++;
      }}
    >
      {count}
    </button>
  );
}
Answercount is a local variable, not React state. Mutating it doesn't trigger a re-render. Fix: use const [count, setCount] = useState(0) and setCount(c => c + 1).

Q11. True/False: React components can return multiple root elements.

AnswerTrue — using Fragments: <>...</> or <React.Fragment>...</React.Fragment>. Without fragments, you'd need a wrapper div.

Q12. Single choice: What does the children prop contain?

AnswerC. children is whatever is placed between <Component> and </Component> tags.

Q13. Matching: Match the hook to its purpose.

HookPurpose
A. useState1. Side effects after render
B. useEffect2. Access DOM element
C. useRef3. Local mutable state
AnswerA→3, B→1, C→2

Q14. Scenario: You render a list of 5 items using index as key. You remove the 2nd item. What problem might occur?

AnswerReact thinks item at index 1 changed (it's now what was index 2), index 2 changed, etc. It updates all items instead of removing one. This can cause incorrect state in components with local state (e.g., an input value stays attached to the wrong item). Fix: use item.id as key.

Q15. True/False: Props are mutable — a child component can modify them.

AnswerFalse. Props are read-only in React. A child should never mutate props. If a child needs to change data, it calls a callback prop provided by the parent.

Junior (15 questions)

Q16. Single choice: What happens if you omit the dependency array from useEffect?

AnswerC. Without a dependency array, the effect runs after every render. This is rarely the desired behavior and can cause performance issues or infinite loops.

Q17. True/False: useMemo guarantees a performance improvement.

AnswerFalse. useMemo adds overhead (storing the value, comparing dependencies). For trivial computations, the memoization cost exceeds the computation cost. Only use it for genuinely expensive operations.

Q18. Debugging: This effect causes an infinite loop. Why?

const [data, setData] = useState([]);
useEffect(() => {
  setData([...data, newItem]);
}, [data]);
AnswersetData creates a new array → data reference changes → effect re-runs → setData again → infinite loop. Fix: use functional update: setData(prev => [...prev, newItem]) and remove data from dependencies.

Q19. Single choice: What is the difference between a controlled and uncontrolled input?

AnswerB. A controlled input's value is driven by React state (value={state}). An uncontrolled input manages its own value in the DOM (defaultValue), accessed via ref.

Q20. Fill in the blank: When context value changes, ___ consumers re-render.

AnswerAll. Every component that calls useContext(MyContext) re-renders when the context value changes, regardless of whether they use the specific part that changed.

Q21. Scenario: You pass an inline function onClick={() => handleClick(id)} to a React.memo child. The child still re-renders every time. Why?

AnswerThe arrow function creates a new function reference on every parent render. React.memo does shallow comparison and sees a "new" prop. Fix: wrap with useCallback: const handler = useCallback(() => handleClick(id), [id]).

Q22. True/False: useRef changes trigger re-renders.

AnswerFalse. Mutating ref.current does NOT trigger a re-render. That's the key difference from useState. Use refs for values that need to persist across renders without causing re-renders (timers, previous values, DOM nodes).

Q23. Multiple choice: Which are valid uses of useEffect? (Select all)

AnswerA and C. B and D should be computed during render (derive directly, or use useMemo). Effects are for side effects that interact with the outside world.

Q24. Scenario: You have 5 levels of prop drilling. What are your options?

Answer 1. Context — for rarely-changing data (theme, auth, locale).
2. Composition — pass components as children/props to skip intermediate levels.
3. External state — Zustand, Jotai (for frequently-changing data).
4. URL state — for shareable/bookmarkable state.
Best approach depends on how often the data changes and how many consumers need it.

Q25. Debugging: This custom hook has a bug. What is it?

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    window.addEventListener('resize', () => setWidth(window.innerWidth));
  }, []);
  return width;
}
AnswerMissing cleanup — the event listener is never removed, causing a memory leak. Fix: return a cleanup function: return () => window.removeEventListener('resize', handler). Also need to assign the handler to a variable to remove the same reference.

Q26. Single choice: How do you prevent useEffect from running on mount?

AnswerB. Effects with dependencies always run on mount (first render). To skip the initial run, use a ref: const isFirst = useRef(true), then check and set it in the effect.

Q27. Fill in the blank: useCallback(fn, [deps]) is equivalent to useMemo(() => _______, [deps]).

Answerfn. useCallback(fn, deps) is syntactic sugar for useMemo(() => fn, deps). Both cache based on dependencies; useCallback specifically caches functions.

Q28. True/False: Context is a good choice for frequently-updating global state like a real-time stock ticker.

AnswerFalse. Context changes re-render all consumers. For frequently-updating state, use an external store like Zustand (which allows selector-based subscriptions) or useSyncExternalStore.

Q29. Scenario: You use useEffect to set state from props:

useEffect(() => {
  setName(props.initialName);
}, [props.initialName]);

What’s wrong?

AnswerThis is an anti-pattern — it creates an unnecessary render cycle (render with old state → effect runs → setState → re-render with new state). Better: derive from props directly, or use key to reset the component: <NameInput key={userId} initialName={name} />.

Q30. Debugging: Your form submits but e.target.value inside the submit handler is undefined. Why?

AnswerIn a submit handler, e.target is the form element, not an input. Use controlled state to access input values, or use new FormData(e.target) to get all form values, or access specific inputs via e.target.elements.fieldName.

Senior / Expert (20 questions)

Q31. Scenario: Your React table freezes with 20,000 rows. What should you investigate first?

Answer 1. First: Are you rendering all 20K DOM nodes? → Virtualize with @tanstack/react-virtual.
2. Second: Are rows re-rendering unnecessarily? → Profile with React DevTools, memoize rows.
3. Third: Is sorting/filtering happening on every render? → Memoize with useMemo.
4. Fourth: Is the data transformation expensive? → Move to a Web Worker.
Virtualization is almost always the answer for large lists.

Q32. True/False: React guarantees that useEffect cleanup runs before the component’s DOM nodes are removed.

AnswerFalse. useEffect cleanup runs after the DOM is updated (it's asynchronous). useLayoutEffect cleanup runs before the DOM is updated (synchronous). This matters for animations, measurements, and focus management.

Q33. Single choice: What happens when you change the key prop on a component?

AnswerB. Changing key tells React "this is a different instance." React unmounts the old component (runs cleanup, destroys state) and mounts a fresh instance. This is a powerful pattern for resetting component state.

Q34. Debugging: A user reports that after navigating to a page and back, the scroll position is lost and data refetches. What’s the architecture issue?

Answer 1. Data refetch: No caching layer. Use React Query with appropriate staleTime to keep data across navigations.
2. Scroll position: Component unmounts on navigation, losing scroll state. Options: restore scroll with scrollRestoration, persist scroll position, or keep the component mounted (layout with outlet).

Q35. Scenario: You have a Server Component that fetches data and passes it to a Client Component. The Client Component needs to refetch on user action. How do you architect this?

Answer 1. Server Component fetches initial data, passes as props.
2. Client Component receives initial data and uses it as initialData for React Query.
3. React Query handles refetching, caching, and loading states client-side.
This gives you the best of both worlds: fast initial load (server-fetched, zero client JS for initial render) + interactive refetching (client-side).

Q36. True/False: In React 18, all setState calls are automatically batched, even in timeouts and promises.

AnswerTrue. React 18 introduced automatic batching for all state updates, regardless of where they occur (event handlers, promises, timeouts, native event handlers). In React 17, only event handlers were batched.

Q37. Single choice: What is the primary purpose of useSyncExternalStore?

AnswerB. useSyncExternalStore ensures consistent reads from external mutable stores during concurrent rendering. Without it, tearing can occur — different parts of the UI reading different versions of the store during a concurrent render.

Q38. Debugging: Your SSR page shows a hydration mismatch warning. The server renders “10:30 AM” but the client renders “10:30 PM”. What’s happening?

AnswerThe server and client are in different time zones. new Date().toLocaleTimeString() produces different output on server vs. client. Fix: use suppressHydrationWarning for this element, render a placeholder on server and update on client, or ensure consistent timezone handling (UTC).

Q39. Design question: You’re building a component library used by 10 teams. How do you decide the component API surface (which props to expose)?

Answer 1. Start minimal — only expose what's needed. It's easier to add props than remove them.
2. Use composition over configuration — prefer <Select><Option /></Select> over <Select options={[...]} />.
3. Support className and style overrides — don't lock down styling.
4. Use discriminated unions for variantsvariant: 'primary' | 'secondary'.
5. Forward refs — consumers may need DOM access.
6. Extend native element propsButtonProps extends ButtonHTMLAttributes.

Q40. Scenario: useTransition is wrapping a state update, but the UI still feels janky. Why?

Answer Possible causes:
1. The rendering itself is expensive (many components) — transitions can interrupt, but each chunk still needs time.
2. The expensive work is in the commit phase (DOM mutations) — transitions only help with render phase.
3. The expensive work is synchronous layout/paint — transitions don't help with CSS/layout costs.
4. The component is not concurrent-safe — it reads from mutable sources that change during render.
Profile to identify whether the bottleneck is React render, DOM mutation, or browser layout/paint.

Q41. True/False: Suspense boundaries catch errors thrown by child components.

AnswerFalse. Suspense catches promises thrown during render (pending state). Error boundaries catch errors. They are different mechanisms. You often need both at the same boundary level.

Q42. Fill in the blank: React’s reconciliation algorithm is O(__) because it uses heuristics: different element types produce different trees, and keys provide stable identity.

Answern. A general tree diff is O(n³). React's heuristic-based approach is O(n) by assuming: 1) different types → different trees, 2) keys provide identity. This is fast at the cost of occasionally unnecessary unmount/remount.

Q43. Scenario: In a concurrent render, your component reads from a global mutable variable that another part of the app modifies. What can go wrong?

AnswerTearing. Concurrent rendering can pause and resume. If the global variable changes between pauses, different parts of the component tree read different values, producing an inconsistent UI. Fix: use useSyncExternalStore to ensure consistent reads, or move the value into React state.

Q44. Design question: You need to choose between putting a complex calculation in useMemo or moving it to a Web Worker. How do you decide?

Answer useMemo: Caches the result, but still runs on the main thread. Good for expensive-but-fast computations (< 16ms).
Web Worker: Runs off the main thread, but has serialization overhead (data must be cloned/transferred). Good for truly heavy work (> 16ms) that would block the UI.
Measure with DevTools. If the computation takes > 1 frame (16ms), consider a Worker. If it's < 5ms but runs often, useMemo is sufficient.

Q45. Debugging: A production app crashes with “Maximum update depth exceeded.” The error points to a component with useEffect. What’s the investigation checklist?

Answer 1. Check if useEffect sets state that's also in its dependency array (effect → setState → re-render → effect → loop).
2. Check for object/array dependencies that are recreated every render (new reference → effect runs → setState → loop).
3. Check for parent re-renders causing prop changes that trigger the effect.
4. Add a counter log in the effect to confirm the loop.
5. Fix: stabilize dependencies with useMemo, use functional setState, or restructure to remove the dependency cycle.

Q46. Scenario: You have a Next.js App Router project. A page loads slowly because it imports a heavy charting library. The chart is below the fold. How do you optimize?

Answer 1. Dynamic import with next/dynamic or React.lazy — don't load the charting library until needed.
2. Intersection Observer — only load when the chart scrolls into view.
3. Suspense boundary — show a skeleton while the chart loads.
4. If the chart is purely visual (no interactivity), consider rendering it as a Server Component or even as an image/SVG.

Q47. True/False: React Server Components can use useState and useEffect.

AnswerFalse. Server Components run only on the server and cannot use hooks that depend on client-side state or lifecycle (useState, useEffect, useContext, etc.). They can use async/await instead. For interactivity, use Client Components ('use client').

Q48. Design question: Your team has 30 engineers contributing to one Next.js app. How do you prevent merge conflicts and ensure consistent architecture?

Answer 1. Feature folder structure — each team owns a feature module with clear boundaries.
2. Shared component library — design system as a shared package.
3. Code owners — CODEOWNERS file for review routing.
4. Architectural Decision Records (ADRs) — document conventions.
5. ESLint rules — enforce import boundaries between features.
6. CI checks — bundle size budgets, type checking, tests.
7. Monorepo (Turborepo) — independent packages reduce coupling.

Q49. Scenario: After upgrading to React 18, some of your tests fail because state updates are now batched differently. How do you fix them?

AnswerReact 18 batches all state updates (including those in promises and timeouts). Tests that relied on intermediate renders between setState calls may fail. Fix: wrap state updates in act(), use waitFor from Testing Library, or update assertions to expect the final batched state.

Q50. Design question: You’re building a real-time collaborative editor (like Google Docs). What React architecture decisions are critical?

Answer 1. State: CRDT or OT library (Yjs, Automerge) — NOT React state for the document model.
2. Rendering: Virtualize the document for large texts. Only render visible portions.
3. Updates: Use useSyncExternalStore to subscribe to the CRDT store.
4. Cursors: Other users' cursors are overlaid, updated via WebSocket.
5. Offline: CRDT handles conflict resolution. Sync on reconnect.
6. Performance: Debounce sync operations. Use Web Workers for CRDT operations.
7. React's role: React renders the UI. The document state lives outside React.

8. Personalized Recommendations

React Core concepts to master first

PriorityConceptWhy
1Render/commit lifecycleFoundation for everything — understand when and why components re-render
2State management patternsKnow when to use local, URL, context, external, and server state
3useEffect correctlyMost misused hook — learn when to use it and when not to
4Composition patternsSenior-level component design — children, slots, compound components
5Reconciliation and keysUnderstand performance at the framework level
6Concurrent renderinguseTransition, Suspense — React’s future
7Server ComponentsThe Next.js App Router model — where React is heading
8Performance profilingDevTools, Profiler, bundle analysis — evidence-based optimization

Advanced topics that matter most for your stack

TopicWhy it matters for you
React Server ComponentsNext.js App Router is built on RSC — essential
Streaming SSRUnderstand how Next.js renders and hydrates
Code splitting with lazy + SuspenseBundle optimization for Next.js and Astro
React Query integrationServer state management in any React app
Design system architectureScale UI across projects
React CompilerWill change how you write components (no more manual memoization)

Common mistakes frontend engineers make

MistakeWhy it happensFix
Over-using useEffectTreating it as a lifecycle methodDerive state, handle in events, use React Query
Global state for everything”It’s easier”Colocate state, use URL state, server state
Not profiling before optimizing”Memoize everything”Profile first, optimize measured bottlenecks
Ignoring accessibility”We’ll add it later”Build accessible from day one
Massive components”Just add another feature”Extract hooks and sub-components early
Testing implementation”Mock useState”Test behavior with Testing Library
Skipping error boundaries”It won’t crash”Every route should have an error boundary
Not code-splitting”The app is small”Split at route level from the start

How to evolve from component developer to frontend architect

Phase 1: Component Mastery
  └── Build any component with proper state, effects, composition

Phase 2: Pattern Recognition
  └── Identify anti-patterns, choose correct state location, optimize renders

Phase 3: System Design
  └── Design component architecture, state architecture, data flow

Phase 4: Performance Engineering
  └── Profile, measure, optimize with evidence. Bundle budgets.

Phase 5: Architecture Leadership
  └── Design system, monorepo strategy, team conventions, ADRs

Phase 6: Framework Understanding
  └── Fiber, scheduler, RSC, compiler. Understand WHY React works this way.

Phase 7: Cross-Cutting Concerns
  └── Accessibility, testing strategy, CI/CD, monitoring, observability

30-day learning plan

Week 1: Rendering Internals (Days 1–7)

DayTaskDeliverable
1Read React docs: “Render and Commit”Mental model diagram
2Build: component that logs render countUnderstanding of re-render triggers
3Profile an app with React DevTools ProfilerIdentify unnecessary re-renders
4Study: reconciliation algorithmCan explain O(n) diff heuristics
5Build: optimize a slow list with React.memoBefore/after profiling comparison
6Study: “children as props” optimization patternImplement in a real component
7Study: fiber architecture overviewCan explain double buffering, work loop

Week 2: Hooks Mastery (Days 8–14)

DayTaskDeliverable
8Read: “You Might Not Need an Effect”List of effects to refactor
9Refactor: replace useEffect-based derived state with direct computationCleaner code
10Build: custom hook with proper cleanup (useMediaQuery)Reusable hook
11Study: useCallback/useMemo — when they help vs. hurtDecision framework
12Build: context + custom hook with memoizationType-safe, optimized context
13Study: useReducer for complex stateImplement a multi-step form
14Build: form with React Hook Form + ZodProduction-ready form

Week 3: Concurrent & Server (Days 15–21)

DayTaskDeliverable
15Study: useTransition and useDeferredValueImplement search with transition
16Build: Suspense with React Query (useSuspenseQuery)Suspense-based data loading
17Study: Error BoundariesImplement route-level error boundaries
18Study: React Server Components modelMental model of server/client boundary
19Build: Next.js page with Server + Client ComponentsWorking RSC page
20Study: Streaming SSR in Next.jsUnderstand how loading.tsx works
21Study: hydration — what it is, what goes wrongCan debug hydration mismatches

Week 4: Architecture (Days 22–30)

DayTaskDeliverable
22Design: state architecture for a medium appArchitecture diagram
23Build: Zustand store with selectorsOptimized global state
24Study: code splitting with lazy + SuspenseImplement route-level splitting
25Study: design system patterns (compound components, Radix)Component API design
26Study: testing strategy (Testing Library + MSW)Integration test for a feature
27Study: bundle analysis (source-map-explorer)Bundle size budget
28Study: accessibility audit (axe, keyboard nav)Accessibility checklist
29Study: React CompilerUnderstand what it automates
30Write: architecture decision record for your projectADR document

Beginner

Intermediate

Advanced

Expert / Internals


Summary, Next Steps, and Advanced Topics

Concise Summary

React is a declarative UI library built on a virtual DOM reconciliation engine. Its core loop is: state changes → component re-renders → diff → minimal DOM mutations. The key to mastery is understanding:

  1. Rendering model: Components are functions. Renders are cheap. Commits are expensive. Don’t fear re-renders — understand them.
  2. State architecture: Colocate state. Use the right tool for each type (local, URL, context, external, server).
  3. Composition: Build UIs from small, focused components. Prefer composition over configuration.
  4. Concurrent rendering: React’s unique advantage. useTransition and Suspense enable responsive UIs under load.
  5. Server Components: React’s future direction. Server-first rendering with selective client interactivity.
  6. Performance: Profile first, optimize second. Virtualize long lists. Split code at routes. Memoize only what’s measured as expensive.

Next Steps

  1. Build a real project using the render/commit mental model — log renders, profile, optimize.
  2. Refactor existing useEffect calls — remove derived state effects, add cleanups.
  3. Implement Suspense + React Query for data fetching.
  4. Migrate a page to Server Components (Next.js App Router).
  5. Build a custom hook library for your team.
  6. Set up bundle size monitoring.

Suggested Advanced Topics

TopicWhy it matters
React Compiler (React Forget)Will eliminate manual memoization
React Server ActionsServer-side mutations from Client Components
Partial Prerendering (PPR)Next.js’s hybrid static/dynamic rendering
Signals debateAlternative reactivity model (Preact, Angular, Solid)
View Transitions APINative browser page transitions
WASM in ReactHeavy computation in the browser
React Native cross-platformShared logic between web and mobile
Micro-frontendsScaling React across teams
Module FederationSharing code between independently deployed apps
Edge renderingReact on Cloudflare Workers / Deno Deploy

Edit page
Share this post:

Previous Post
Cloudflare Workers — Complete Deep-Dive Engineering Guide
Next Post
🔐 Deep Dive: CORS & PROXY — Toàn bộ kiến thức từ cơ bản đến nâng cao