ReactJs Under the Hood

Understanding how React works internally helps developers write more performant applications and debug complex issues. React’s architecture has evolved significantly over the years, with the Fiber reconciliation engine representing a major rewrite that enables features like concurrent rendering and time-slicing.

Virtual DOM and Reconciliation

React uses a Virtual DOM, an in-memory representation of the actual DOM. When component state changes, React:

  1. Creates a new Virtual DOM tree
  2. Compares it with the previous tree (reconciliation)
  3. Calculates minimal changes needed
  4. Updates only changed parts of the real DOM

This approach is faster than manipulating the DOM directly for complex UIs, as DOM operations are expensive while JavaScript object comparisons are cheap.

The Reconciliation Algorithm

React’s reconciliation algorithm makes assumptions to achieve O(n) complexity instead of O(n³):

Assumption 1: Elements of different types produce different trees

// React will destroy the old <div> and create a new <span>
<div><Counter /></div>

<span><Counter /></span>

Assumption 2: Developers can hint at stable children with keys

// Without keys, React might recreate all elements
<ul>
  {items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>

Assumption 3: Component instances maintain identity across renders when the type and key don’t change

The Fiber Architecture

React 16 introduced Fiber, a complete rewrite of React’s reconciliation algorithm. Fiber is a JavaScript object representing a unit of work, forming a linked-list tree structure.

What is a Fiber?

Each Fiber represents a component instance and contains:

  • Component type and props
  • State and hooks
  • Pointers to child, sibling, and parent Fibers
  • Effect lists (side effects to perform)
  • Priority and expiration time
// Simplified Fiber structure
{
  type: 'div',                  // Component type
  key: null,                    // Unique key
  props: { className: 'app' },  // Props
  stateNode: <DOMElement>,      // Actual DOM node
  child: <Fiber>,               // First child Fiber
  sibling: <Fiber>,             // Next sibling Fiber
  return: <Fiber>,              // Parent Fiber
  alternate: <Fiber>,           // Previous Fiber for comparison
  effectTag: 'UPDATE',          // Type of change
  nextEffect: <Fiber>           // Next Fiber with effects
}

The Fiber Tree

React maintains two Fiber trees:

  • Current tree: Represents the current UI
  • Work-in-progress tree: Being constructed during updates

After completing work, React swaps the work-in-progress tree to become the current tree (double buffering).

Render Phases

React’s rendering process has two distinct phases:

1. Render Phase (Reconciliation)

The render phase is pure and can be paused, aborted, or restarted:

  • Begin work: React walks down the tree, creating or updating Fibers
  • React calls component functions and lifecycle methods
  • Calculates which Fibers need changes
  • Builds effect lists

This phase can be interrupted and resumed, enabling concurrent features.

2. Commit Phase

The commit phase is synchronous and cannot be interrupted:

  • Complete work: React walks up the tree, finalizing Fibers
  • Commit: React applies changes to the DOM
  • Runs layout effects (useLayoutEffect)
  • Runs passive effects (useEffect) after painting

Concurrent Rendering

React 18 introduced concurrent rendering, allowing React to work on multiple state updates simultaneously and interrupt rendering for higher-priority updates.

Time Slicing

React breaks rendering work into chunks and yields to the browser periodically:

function ExpensiveList({ items }) {
  // With concurrent rendering, React can pause rendering this list
  // to handle user input, then resume
  return (
    <ul>
      {items.map(item => (
        <ExpensiveItem key={item.id} item={item} />
      ))}
    </ul>
  );
}

Priority Levels

React assigns priority to updates:

  • Immediate: User interactions (clicks, typing)
  • Normal: Network responses, timers
  • Low: Data fetching, analytics

Higher-priority updates can interrupt lower-priority ones.

Transitions

The useTransition Hook marks updates as transitions, allowing React to defer them:

import { useState, useTransition } from 'react';

function SearchResults() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();
  
  const handleChange = (e) => {
    // Update input immediately
    setQuery(e.target.value);
    
    // Mark results update as a transition
    startTransition(() => {
      setResults(expensiveSearch(e.target.value));
    });
  };
  
  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending ? <Spinner /> : <List items={results} />}
    </>
  );
}

React Internals Workflow

Creating a New Feature (Live Coding)

Dan Abramov’s live coding session demonstrates implementing a new React feature. Key insights:

React’s package structure:

  • Each package has its own responsibility
  • Source code lives in packages/*/src
  • Tests can be anywhere but are often co-located
  • The codebase is complex but well-organized

Development workflow:

  • Test-first development is encouraged
  • Start with a failing test for the desired behavior
  • Implement the feature to make the test pass
  • Consider edge cases and refactor

Key internal modules:

  • ReactFiberBeginWork: Handles the “begin” phase of reconciliation
  • ReactFiberCompleteWork: Handles the “complete” phase
  • ReactFiberCommitWork: Applies changes to the DOM
  • ReactWorkTags: Defines all possible Fiber types

Important concepts:

  • Fibers are reused between renders for performance
  • React 16+ creates a copy of the initial Fiber (current ↔ work-in-progress)
  • The reconciliation process has three main steps: begin, complete, commit

Prefer Fragments Over Divs

For wrapping multiple elements without styles, use Fragments instead of divs:

// Less optimal - creates unnecessary DOM node
return (
  <div>
    <Child1 />
    <Child2 />
  </div>
);

// Better - no extra DOM node
return (
  <>
    <Child1 />
    <Child2 />
  </>
);

Server Components

React Server Components (RSC) allow components to render on the server, sending only the rendered output to the client:

// Server Component (default)
async function BlogPost({ id }) {
  const post = await db.posts.find(id);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <Comments postId={id} /> {/* Can be a Client Component */}
    </article>
  );
}

// Client Component (opt-in with 'use client')
'use client';

function Comments({ postId }) {
  const [comments, setComments] = useState([]);
  
  useEffect(() => {
    fetchComments(postId).then(setComments);
  }, [postId]);
  
  return <CommentList comments={comments} />;
}

Benefits:

  • Reduced bundle size (server components don’t ship to client)
  • Direct database access without API layer
  • Automatic code splitting
  • Streaming server rendering

Discussed in: React Server Components with Dan Abramov, Joe Savona, and Kent C. Dodds

React 19 Features

React 19 introduces several new capabilities:

Actions

Actions simplify handling async operations:

function UpdateForm() {
  const [error, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const response = await updateUser(formData);
      if (response.error) {
        return { error: response.error };
      }
      return { success: true };
    },
    { error: null }
  );
  
  return (
    <form action={submitAction}>
      <input name="name" />
      <button disabled={isPending}>
        {isPending ? 'Saving...' : 'Save'}
      </button>
      {error && <p>{error}</p>}
    </form>
  );
}

use Hook

The use Hook reads resources like Promises and Contexts:

function UserProfile({ userPromise }) {
  const user = use(userPromise);
  
  return <div>{user.name}</div>;
}

Debugging React Internals

React DevTools

React DevTools provides insight into the component tree, props, state, and performance:

  • Components tab: Inspect component hierarchy and state
  • Profiler tab: Measure rendering performance
  • Highlight updates: Visualize which components re-render

Strict Mode

StrictMode helps identify potential problems by intentionally double-invoking functions:

<StrictMode>
  <App />
</StrictMode>

In development, Strict Mode:

  • Calls component functions twice
  • Runs effects twice
  • Checks for deprecated APIs
  • Warns about unsafe lifecycles

Resources