React Complete Guide | Fiber, Virtual DOM, Hooks Internals, Concurrency & Production Patterns

React Complete Guide | Fiber, Virtual DOM, Hooks Internals, Concurrency & Production Patterns

이 글의 핵심

A single deep guide connecting React usage with how Fiber reconciliation, virtual DOM diffing, hooks on fibers, the Concurrent scheduler, and production patterns actually fit together.

Key takeaways

React lets you describe UI as a function of state and components. Internally, Fiber-based reconciliation and virtual DOM diffing compute the smallest set of changes to apply to the real DOM (or another host). This article pairs everyday API usage with how the work loop traverses Fiber nodes, how list diffing uses keys, how hooks hang off memoizedState, how the Scheduler prioritizes updates, and patterns that survive real production traffic.

Practical angle: dashboards, chat, and large tables punish both “too much tree work” and “janky main-thread stalls.” Narrowing render scope, using transitions/Suspense intentionally, and measuring before memoizing beats folklore micro-optimizations.


1. What React is

React recomputes a React element tree whenever state changes, compares it with the previous tree, and applies minimal mutations to the underlying tree (DOM, native views, Canvas, etc.). The core package is renderer-agnostic; react-dom, react-native, and others implement the host-specific “commit” operations.

One-liner: you declare what the UI should be; React figures out how to update it efficiently.


2. Project setup

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev

On React 18+, mount with createRoot:

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>
);

StrictMode intentionally double-invokes certain lifecycles in development to surface unsafe side effects. It has no production runtime impact.


3. Components, JSX, and basic hooks

Function components use hooks for state and side effects:

import { useState, useEffect, useMemo, useCallback } from 'react';

type User = { id: string; name: string };

export function UserPanel({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      const res = await fetch(`/api/users/${userId}`);
      const data: User = await res.json();
      if (!cancelled) setUser(data);
    })();
    return () => {
      cancelled = true;
    };
  }, [userId]);

  const title = useMemo(() => (user ? `Hello, ${user.name}` : 'Loading…'), [user]);
  const onRetry = useCallback(() => {
    setUser(null);
  }, []);

  return <section aria-label={title}>{user?.name ?? '…'}</section>;
}
  • The dependency array is a contract: “re-run this effect when these values change.” Pairing cleanup with cancelled avoids races when userId changes quickly.
  • useMemo / useCallback matter when referential identity matters—e.g. children wrapped in React.memo. Profile first; blanket memoization often hurts readability for no gain.

4. Fiber reconciliation (deep dive)

React 16 replaced the stack reconciler with a Fiber reconciler. Goals: (1) chunk work so the main thread is not monopolized, (2) prioritize updates, and (3) support interruptible, resumable, discardable render passes—the foundation of Concurrent features.

4.1 Render phase vs commit phase

  • Render (reconciliation) phase: builds the workInProgress Fiber tree and computes what must change. It should be pure (derive from props/state only) and may be interrupted.
  • Commit phase: performs real host mutations (DOM updates), layout/paint-affecting work. It is synchronous and not interruptible.

That split is why you must not cause observable side effects during render: the render pass might run more than once or abort before commit (especially under StrictMode or Concurrent rendering).

4.2 Fiber nodes and the linked-list tree

Each component instance / host node corresponds to a Fiber. Conceptually it carries:

  • child / sibling / return: pointers that encode a depth-first traversal without an explicit stack, making it easy to slice work into units.
  • alternate: links the current tree and work-in-progress tree for double buffering. When work completes, trees swap at commit.
  • memoizedProps / memoizedState: finalized props for this render and the head of the hook list (among other memoized values).

Think of a Fiber as both “what to show” and a schedulable work item.

4.3 workLoop and time slicing

The scheduler processes Fibers in small slices, yielding when a frame budget is exhausted so the browser can handle input and paint—this is the basis of time slicing. The reconciler is less “compare two giant trees in one shot” and more incremental work loop over Fibers.

4.4 beginWork / completeWork and child reconciliation

Roughly, each Fiber passes through beginWork (decide what to do, descend to children) and completeWork (bubble up and finalize). Host components (e.g. div) diff props then attach child Fibers; function/class components run the component to obtain child elements.

When reconciling children, React treats keyed and non-keyed children differently. Without keys, children are matched by position; with keys, React can build maps and match in roughly O(n) time. That is why stable keys on lists are a rule, not a style preference.

4.5 Effect lists and commit sub-phases

Commit is not atomic: mutate DOM → useLayoutEffect (sync, layout) → paint → useEffect (async after paint). Use useLayoutEffect only when you must read/write layout before paint; most subscriptions and logging belong in useEffect to avoid blocking the main thread.


5. Virtual DOM and diffing (deep dive)

The virtual DOM is an in-memory tree of lightweight React elements. Each render constructs a fresh element tree; React compares it with the previous Fiber snapshot to produce a minimal change set.

5.1 Different element types

If the root type changes (divsection, or a different component function), React replaces the subtree. Internal state is usually lost, so swapping unrelated component types at the same position is a common footgun.

5.2 Same host element type

For the same intrinsic tag, React patches props in place—className, style, event handlers, etc.—instead of rebuilding everything.

5.3 Child lists and keys

Among siblings, key identifies stable identity across renders. React uses keys to match old and new children efficiently.

  • Using array index as key breaks when items are reordered, inserted, or removed: the wrong instances may be reused, causing jumpy focus or state mis-attachment.
  • Prefer stable domain ids (database ids, natural keys).

5.4 Heuristics and cost

The diff is heuristic and tuned for typical UI patterns (O(n)-ish behavior). Still, deep, wide trees that fully re-render often are expensive. Production mitigations: narrow state, memoized subtrees where measured, virtualized lists, and route-level code splitting to shrink the amount of work per update.

5.5 “Same position” rules and component identity

Reconciliation is keyed off child slots under a parent. If the same slot receives the same component function reference, React treats it as the same instance and preserves state. If you swap two different component types in the same slot, React sees a type change and drops state. To preserve state across different UIs, branch inside one component or split instances with an explicit key.


6. Hooks implementation (deep dive)

Hooks are not magic—they are a linked list stored on the Fiber’s memoizedState. Each hook call creates a node; the next hook chains via next.

6.1 useState and dispatchers

useState stores the current value, a queue of updates, and links to the next hook. Calling the setter schedules an update on that Fiber, and the scheduler flushes a render when appropriate.

6.2 Why hook order must be fixed

Conditional or looped hook calls change the number of hook nodes per render, desynchronizing the list and pairing state with the wrong hook—this is the mechanical reason behind the Rules of Hooks.

6.3 useEffect queues

Effects run after paint (asynchronously in the browser sense), and dependencies decide whether to tear down and re-subscribe. For synchronous DOM reads/writes that must happen before paint, useLayoutEffect is the tool—use sparingly because it blocks painting.

6.4 Mount vs update dispatchers

Internally React maintains a current dispatcher per render phase. Built-in hooks like useState take different paths on first mount vs updates: mount initializes hook nodes; update enqueues changes on existing queues. Custom hooks must follow the same rules—conditional hooks inside a custom hook still break the hook list if call counts change.

6.5 useReducer and batching

useReducer is ideal for event-heavy state machines. React 18 automatic batching applies broadly (including inside promises and timeouts), so multiple setState calls in the same tick often collapse into one re-render, which pairs well with splitting state without multiplying renders.


7. Concurrent rendering (deep dive)

createRoot enables concurrent features, but not every update is “low priority.” React models urgency with lanes (an internal priority system).

  • Urgent updates: typing, clicks—need immediate feedback.
  • Transition updates: wrapped in startTransition—can lag behind.

useTransition / useDeferredValue keep input snappy while deferring expensive derived rendering. Together with Suspense, you can express loading boundaries for data and code.

Caveat: because renders may retry or abort, never put side effects in render. Fetch in effects or data libraries; write code that survives StrictMode’s development-only double invocation.

7.2 Lanes and starvation

Updates are grouped into lanes (internal priorities). Urgent input can keep delaying a heavy transition; the scheduler still tries to ensure long-waiting work eventually runs. Even so, a massive transition (re-rendering a huge list) may need more than useDeferredValue—pair transitions with chunking and virtualization.

7.3 useSyncExternalStore and external stores

When wiring global stores (Redux, Zustand, etc.), useSyncExternalStore is the React 18-friendly pattern to avoid tearing—where different parts of the UI read inconsistent snapshots during concurrent rendering. Subscription consistency matters more once renders can interleave.


8. Production React patterns

8.1 Boundaries and recovery

  • Error boundaries catch render/lifecycle errors in descendants and show fallback UI (not errors inside event handlers—handle those explicitly).
  • Suspense defines loading boundaries; pair with routers and React.lazy for route-level splitting.

8.2 State architecture

  • Separate server state (cache, invalidation) from UI state (modals, ephemeral form fields).
  • Avoid premature global stores; colocate state at the lowest shared ancestor.

8.3 Performance habits

  • Lists: stable key, normalized stores (byId) when data is relational.
  • Memoization: apply memo / useMemo to measured hot children; useless memo on components whose props change every render buys nothing.
  • Bundles: dynamic import(), tree-shakeable imports, avoid pulling entire heavy libraries for one helper.

8.4 Accessibility and forms

Default to semantic HTML, focus management, and ARIA where needed. For complex forms, separate controlled vs uncontrolled concerns and map validation, client, and server errors consistently (libraries like React Hook Form help).

8.5 Composition, context, and performance

  • Composition: children or slot props reduce prop drilling while keeping types precise.
  • Context: putting a frequently changing value in a large context can re-render every consumer. Prefer small contexts or external stores with useSyncExternalStore for volatile data.
  • Render props / custom hooks: when only logic is shared, extract a custom hook—easier to test and reuse than nested HOCs.

8.6 Server Components and framework boundaries

With frameworks like the Next.js App Router, React Server Components can move data fetching and heavy dependencies to the server, shrinking the client bundle. Keep client components for interactivity and browser APIs; clear boundaries lighten both the Fiber tree and the network payload.

8.7 Testing and observability

  • Unit: hooks and pure utils with Jest/Vitest; components with Testing Library (user-centric assertions).
  • E2E: Playwright/Cypress for critical journeys.
  • Production: error boundaries + logging (e.g. Sentry), Core Web Vitals, React DevTools Profiler to find render hotspots.

9. Summary

React combines declarative UI with a Fiber-based, interruptible reconciliation pipeline, heuristic virtual DOM diffing, hook state stored on Fibers, and a Scheduler that can prioritize urgent work. Understanding that stack answers why StrictMode double-invokes in dev, why hook order is fixed, and when transitions help more than ad hoc debouncing. Layer error and suspense boundaries, disciplined state boundaries, and measurement-driven optimization for production-grade apps.


Further reading

  • For more on React 18 APIs and Suspense patterns, see the React 18 deep dive article on this blog.
  • Pair with the TypeScript 5 guide when typing component props and app-level modules.