Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save carefree-ladka/a91d2bb9e731747d0f363fbcfc7c3b2c to your computer and use it in GitHub Desktop.

Select an option

Save carefree-ladka/a91d2bb9e731747d0f363fbcfc7c3b2c to your computer and use it in GitHub Desktop.
React Internals: JSX, Reconciliation & Virtual DOM

React Internals: JSX, Reconciliation & Virtual DOM

A deep dive into how React works under the hood, with visual diagrams and practical examples.


Table of Contents

  1. JSX → React.createElement

    • JSX Transformation Example
    • React.createElement Signature
    • Complex Example
    • What React.createElement Returns
    • JSX Compilation Flow
  2. Virtual DOM vs Real DOM

    • Real DOM vs Virtual DOM Comparison
    • Virtual DOM Structure
    • Example: Virtual DOM Object
  3. Diffing Algorithm

    • Diffing Heuristics
    • Type Change Example
    • Same Type Diffing
  4. Keys and List Reconciliation

    • Without Keys (Bad)
    • With Keys (Good)
    • Key Rules
  5. Reconciliation Process

    • High-Level Reconciliation Flow
    • Step-by-Step Example
    • Reconciliation Phases
  6. Fiber Architecture

    • What is a Fiber?
    • Fiber Tree Structure
    • Work Loop
    • Why Fiber?
  7. Putting It All Together

    • Complete React Update Cycle
    • Real-World Example
  8. Performance Implications

    • Best Practices
  9. Summary


1. JSX → React.createElement

JSX is syntactic sugar that gets transformed into React.createElement calls during the build process.

JSX Transformation Example

// What you write:
const element = <div className="container">Hello World</div>;

// What it becomes:
const element = React.createElement(
  'div',
  { className: 'container' },
  'Hello World'
);

React.createElement Signature

React.createElement(type, props, ...children)
  • type: String (HTML tag) or Component (function/class)
  • props: Object with properties (null if none)
  • children: Child elements or text

Complex Example

// JSX:
<div className="card">
  <h1>Title</h1>
  <p>Description</p>
  <Button onClick={handleClick}>Click me</Button>
</div>

// Transformed to:
React.createElement(
  'div',
  { className: 'card' },
  React.createElement('h1', null, 'Title'),
  React.createElement('p', null, 'Description'),
  React.createElement(
    Button,
    { onClick: handleClick },
    'Click me'
  )
);

What React.createElement Returns

It returns a React Element - a plain JavaScript object:

{
  type: 'div',
  props: {
    className: 'card',
    children: [
      { type: 'h1', props: { children: 'Title' } },
      { type: 'p', props: { children: 'Description' } },
      { type: Button, props: { onClick: fn, children: 'Click me' } }
    ]
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element')
}

JSX Compilation Flow

graph LR
    A[JSX Code] -->|Babel/Compiler| B[React.createElement calls]
    B -->|Runtime| C[React Elements]
    C -->|React Renderer| D[Fiber Nodes]
    D -->|Reconciliation| E[DOM Updates]
Loading

2. Virtual DOM vs Real DOM

The Virtual DOM is React's lightweight representation of the actual DOM.

Real DOM vs Virtual DOM

Aspect Real DOM Virtual DOM
Nature Browser's actual DOM tree JavaScript object representation
Updates Expensive (triggers reflow/repaint) Cheap (just object manipulation)
Speed Slow for frequent updates Fast diffing and batching
Memory Heavy Lightweight

Virtual DOM Structure

graph TD
    A[Virtual DOM Tree] --> B[div#root]
    B --> C[div.container]
    C --> D[h1 'Hello']
    C --> E[ul]
    E --> F[li 'Item 1']
    E --> G[li 'Item 2']
    
    style A fill:#e1f5ff
    style B fill:#fff4e1
    style C fill:#ffe1e1
Loading

Example: Virtual DOM Object

// Virtual DOM representation
{
  type: 'div',
  props: {
    className: 'container',
    children: [
      {
        type: 'h1',
        props: { children: 'Hello' }
      },
      {
        type: 'ul',
        props: {
          children: [
            { type: 'li', props: { children: 'Item 1' } },
            { type: 'li', props: { children: 'Item 2' } }
          ]
        }
      }
    ]
  }
}

3. Diffing Algorithm

React uses a heuristic O(n) algorithm instead of the traditional O(n³) tree diffing.

Diffing Heuristics

React makes two assumptions:

  1. Different types produce different trees - If element type changes, rebuild from scratch
  2. Keys identify which children have changed - Use keys to match elements across renders

Type Change Example

graph LR
    subgraph "Old Tree"
    A1[div] --> B1[span 'Hello']
    end
    
    subgraph "New Tree"
    A2[p] --> B2[span 'Hello']
    end
    
    A1 -.->|Type Changed| A2
    
    style A1 fill:#ffcccc
    style A2 fill:#ccffcc
Loading

Result: Entire subtree is destroyed and rebuilt because divp

// Before:
<div><span>Hello</span></div>

// After:
<p><span>Hello</span></p>

// React destroys div and span, creates new p and span

Same Type Diffing

graph TD
    subgraph "Old"
    A1[div.old] --> B1[span 'Text']
    end
    
    subgraph "New"
    A2[div.new] --> B2[span 'Text']
    end
    
    A1 -.->|Update Props| A2
    B1 -.->|Keep| B2
    
    style A1 fill:#fff4cc
    style A2 fill:#ccffcc
Loading

Result: Keep the DOM node, update only changed attributes

// Before:
<div className="old" style={{color: 'red'}}>Text</div>

// After:
<div className="new" style={{color: 'blue'}}>Text</div>

// React only updates className and style, keeps the DOM node

4. Keys and List Reconciliation

Keys help React identify which items have changed, been added, or removed.

Without Keys (Bad)

// Initial render:
<ul>
  <li>Alice</li>
  <li>Bob</li>
</ul>

// After adding Charlie at the beginning:
<ul>
  <li>Charlie</li>
  <li>Alice</li>
  <li>Bob</li>
</ul>

What React does: Updates all three <li> elements (inefficient!)

graph LR
    subgraph "Before"
    A1[li: Alice] --> A2[li: Bob]
    end
    
    subgraph "After"
    B1[li: Charlie] --> B2[li: Alice] --> B3[li: Bob]
    end
    
    A1 -.->|Update to Charlie| B1
    A2 -.->|Update to Alice| B2
    B3[li: Bob]
    
    style B1 fill:#ffcccc
    style B2 fill:#ffcccc
    style B3 fill:#ccffcc
Loading

With Keys (Good)

// Initial render:
<ul>
  <li key="alice">Alice</li>
  <li key="bob">Bob</li>
</ul>

// After adding Charlie:
<ul>
  <li key="charlie">Charlie</li>
  <li key="alice">Alice</li>
  <li key="bob">Bob</li>
</ul>

What React does: Recognizes Alice and Bob unchanged, only inserts Charlie

graph LR
    subgraph "Before"
    A1[li key=alice] --> A2[li key=bob]
    end
    
    subgraph "After"
    B1[li key=charlie] --> B2[li key=alice] --> B3[li key=bob]
    end
    
    A1 -.->|Move| B2
    A2 -.->|Move| B3
    B1[NEW]
    
    style B1 fill:#ccffcc
    style B2 fill:#ccccff
    style B3 fill:#ccccff
Loading

Key Rules

// ✅ Good: Stable, unique keys
items.map(item => <div key={item.id}>{item.name}</div>)

// ❌ Bad: Index as key (unstable when reordering)
items.map((item, index) => <div key={index}>{item.name}</div>)

// ❌ Bad: Random keys (creates new elements every render)
items.map(item => <div key={Math.random()}>{item.name}</div>)

5. Reconciliation Process

Reconciliation is the algorithm React uses to diff one tree with another to determine what needs to change.

High-Level Reconciliation Flow

graph TD
    A[State/Props Change] --> B[Re-render Component]
    B --> C[Create New Virtual DOM]
    C --> D[Diff with Old Virtual DOM]
    D --> E{Changes Found?}
    E -->|Yes| F[Create Update Queue]
    E -->|No| G[Do Nothing]
    F --> H[Batch Updates]
    H --> I[Commit to Real DOM]
    
    style A fill:#ffcccc
    style C fill:#cce5ff
    style D fill:#ffffcc
    style I fill:#ccffcc
Loading

Step-by-Step Example

function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

Step 1: Initial Render

graph TD
    A[Counter Component] --> B[Virtual DOM]
    B --> C[div]
    C --> D["p: 'Count: 0'"]
    C --> E["button: '+'"]
    B --> F[Real DOM]
    F --> G[div]
    G --> H["p: 'Count: 0'"]
    G --> I["button: '+'"]
Loading

Step 2: State Update (count = 1)

graph TD
    A[setCount Called] --> B[New Virtual DOM]
    B --> C[div]
    C --> D["p: 'Count: 1'"]
    C --> E["button: '+'"]
    
    B --> F[Diff Algorithm]
    F --> G{Compare Trees}
    G -->|div: Same| H[Keep]
    G -->|p text: Changed| I[Update Text Node]
    G -->|button: Same| J[Keep]
    
    I --> K[DOM Update]
    K --> L[Only update text content of p]
    
    style A fill:#ffcccc
    style F fill:#ffffcc
    style L fill:#ccffcc
Loading

Reconciliation Phases

React 16+ uses Fiber architecture with two phases:

graph LR
    A[Render Phase] -->|Interruptible| B[Build Work-in-Progress Tree]
    B --> C[Diff & Mark Updates]
    C --> D[Commit Phase]
    D -->|Non-Interruptible| E[Apply DOM Updates]
    E --> F[Run Effects]
    
    style A fill:#cce5ff
    style D fill:#ccffcc
Loading

6. Fiber Architecture

Fiber is React's reconciliation engine since version 16. It's a reimplementation of the core algorithm.

What is a Fiber?

A Fiber is a JavaScript object representing a unit of work. Each React element has a corresponding Fiber node.

// Simplified Fiber node structure
{
  type: 'div',              // Component type
  key: null,                // Key from props
  props: { children: [...] }, // Props
  stateNode: DOMNode,       // Actual DOM node
  return: parentFiber,      // Parent fiber
  child: firstChildFiber,   // First child
  sibling: nextSiblingFiber, // Next sibling
  alternate: oldFiber,      // Previous version
  effectTag: 'UPDATE',      // What needs to be done
  nextEffect: nextFiber     // Linked list of effects
}

Fiber Tree Structure

graph TD
    A[App Fiber] -->|child| B[div Fiber]
    B -->|child| C[h1 Fiber]
    C -->|sibling| D[p Fiber]
    
    B -->|return| A
    C -->|return| B
    D -->|return| B
    
    A -.->|alternate| A2[Old App Fiber]
    B -.->|alternate| B2[Old div Fiber]
    
    style A fill:#e1f5ff
    style B fill:#fff4e1
    style C fill:#ffe1f0
    style D fill:#ffe1f0
    style A2 fill:#f0f0f0
    style B2 fill:#f0f0f0
Loading

Work Loop

Fiber enables time-slicing: breaking work into chunks and spreading it across multiple frames.

graph LR
    A[Work Loop] --> B{More Work?}
    B -->|Yes| C{Time Remaining?}
    C -->|Yes| D[Process Next Fiber]
    C -->|No| E[Yield to Browser]
    E --> A
    D --> A
    B -->|No| F[Commit Phase]
    
    style E fill:#ffffcc
    style F fill:#ccffcc
Loading
// Simplified work loop concept
function workLoop(deadline) {
  let shouldYield = false;
  
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }
  
  if (nextUnitOfWork) {
    // More work to do, schedule next frame
    requestIdleCallback(workLoop);
  } else {
    // All work done, commit to DOM
    commitRoot();
  }
}

Why Fiber?

Before Fiber (Stack Reconciler):

  • Synchronous, recursive
  • Once started, couldn't pause
  • Long updates blocked the browser

With Fiber:

  • Asynchronous, can pause/resume
  • Prioritize urgent updates (user input)
  • Better perceived performance
graph TD
    subgraph "Stack Reconciler"
    A1[Start Update] --> B1[Process Entire Tree]
    B1 --> C1[Commit]
    C1 --> D1[Browser Can Render]
    end
    
    subgraph "Fiber Reconciler"
    A2[Start Update] --> B2[Process Chunk 1]
    B2 --> C2[Yield to Browser]
    C2 --> D2[Process Chunk 2]
    D2 --> E2[Yield to Browser]
    E2 --> F2[Commit]
    end
    
    style B1 fill:#ffcccc
    style C2 fill:#ccffcc
    style E2 fill:#ccffcc
Loading

7. Putting It All Together

Complete React Update Cycle

graph TD
    A[User Action / setState] --> B[Schedule Update]
    B --> C[Render Phase Begins]
    C --> D[Component Returns JSX]
    D --> E[JSX → React.createElement]
    E --> F[Create React Elements]
    F --> G[Build Fiber Tree]
    G --> H[Diff with Current Tree]
    H --> I[Mark Effects UPDATE/DELETE/INSERT]
    I --> J{More Work?}
    J -->|Yes| K{Has Time?}
    K -->|Yes| C
    K -->|No| L[Yield to Browser]
    L --> C
    J -->|No| M[Commit Phase]
    M --> N[Apply DOM Mutations]
    N --> O[Run useLayoutEffect]
    O --> P[Browser Paints]
    P --> Q[Run useEffect]
    
    style A fill:#ffcccc
    style G fill:#cce5ff
    style H fill:#ffffcc
    style N fill:#ccffcc
    style P fill:#ccffcc
Loading

Real-World Example

function TodoApp() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React' },
    { id: 2, text: 'Build App' }
  ]);
  
  const addTodo = () => {
    setTodos([...todos, { id: 3, text: 'Deploy' }]);
  };
  
  return (
    <div>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
      <button onClick={addTodo}>Add</button>
    </div>
  );
}

When addTodo is clicked:

  1. setState triggers update
  2. JSX transformed to React.createElement calls
  3. Virtual DOM new tree created with 3 todos
  4. Diffing compares old (2 todos) vs new (3 todos)
  5. Keys identify that todos 1 & 2 unchanged
  6. Fiber marks effect: INSERT new <li> with id=3
  7. Commit inserts single DOM node
  8. Result efficient update, only one DOM insertion

8. Performance Implications

Best Practices

1. Use Keys for Lists

// ✅ Efficient reconciliation
<ul>
  {items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>

2. Avoid Inline Functions/Objects in JSX

// ❌ Creates new function every render
<button onClick={() => handleClick(id)}>Click</button>

// ✅ Memoized callback
const handleClickMemoized = useCallback(() => handleClick(id), [id]);
<button onClick={handleClickMemoized}>Click</button>

3. Prevent Unnecessary Re-renders

// ✅ Memoize expensive components
const MemoizedChild = React.memo(Child);

// ✅ Use React.memo with custom comparison
const MemoizedItem = React.memo(Item, (prev, next) => {
  return prev.id === next.id && prev.text === next.text;
});

4. Keep Component Type Stable

// ❌ Creates new component type every render
function Parent() {
  const Child = () => <div>Child</div>;
  return <Child />;
}

// ✅ Stable component reference
const Child = () => <div>Child</div>;
function Parent() {
  return <Child />;
}

Summary

  • JSX is syntactic sugar for React.createElement calls
  • React Elements are plain JavaScript objects describing UI
  • Virtual DOM is a lightweight representation enabling efficient updates
  • Diffing uses heuristics (type comparison, keys) for O(n) performance
  • Fiber enables interruptible rendering and better performance
  • Keys are critical for efficient list reconciliation
  • Reconciliation compares trees and produces minimal DOM updates

Understanding these internals helps you write more performant React applications and debug issues more effectively.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment