About Posts Series Contact
Deep Dive 4 min read

Building a Custom React Renderer from Scratch

How React-reconciler works, why you'd ever want to build your own renderer, and a working implementation that renders to a terminal canvas.

Honey Sharma

Most React developers have a mental model that looks like this: React → DOM. That’s the happy path. But React’s actual architecture is a two-layer system: the reconciler (which manages component trees and diffs) and the renderer (which applies those diffs to some host environment). The DOM is just one possible target.

Why Build a Custom Renderer?

The honest answer: you probably don’t need to. But the process of building one reveals things about React’s internals that no amount of reading documentation will give you.

Practical reasons you might:

  • Ink — renders React to the terminal (and it’s actually great)
  • React Three Fiber — renders React to Three.js scene graph
  • React PDF — generates PDFs from JSX
  • Testing — full control over the host environment

The react-reconciler Package

React exposes its reconciliation algorithm through a package: react-reconciler. You implement a “host config” object that tells the reconciler how to interact with your target environment.

import ReactReconciler from 'react-reconciler';

const hostConfig = {
  // Create an instance of a host element
  createInstance(type, props, rootContainer, hostContext, internalHandle) {
    return createElement(type, props);
  },

  // Create text nodes
  createTextInstance(text, rootContainer, hostContext, internalHandle) {
    return createTextNode(text);
  },

  // Append a child to a parent
  appendChildToContainer(container, child) {
    container.appendChild(child);
  },

  // Called after all children are mounted
  finalizeInitialChildren(instance, type, props, rootContainer, hostContext) {
    return false; // return true to trigger commitMount
  },

  // Required but can be empty for simple renderers
  prepareUpdate() { return null; },
  commitUpdate() {},

  // Scheduler (use React's default)
  scheduleTimeout: setTimeout,
  cancelTimeout: clearTimeout,
  noTimeout: -1,

  // Feature flags
  supportsMutation: true,
  supportsHydration: false,
  isPrimaryRenderer: true,
};

const reconciler = ReactReconciler(hostConfig);

Building the Terminal Renderer

Our target: render React components to a terminal using ANSI escape codes. Each “DOM node” is a rectangular box with position, size, and content.

interface TerminalBox {
  type: 'box' | 'text';
  x: number;
  y: number;
  width: number;
  height: number;
  content?: string;
  color?: string;
  children: TerminalBox[];
}

The Update Lifecycle

Here’s the part that trips people up. React’s commit phase has three sub-phases:

  1. Before MutationgetSnapshotBeforeUpdate equivalent
  2. Mutation — where DOM changes actually happen
  3. LayoutcomponentDidMount / useLayoutEffect

Each phase calls different host config methods. The reconciler guarantees that children are committed before parents in the mutation phase.

// These fire in order:
prepareForCommit(container)
// ... mutation phase ...
commitMount(instance, type, newProps, ...)  // if finalizeInitialChildren returned true
// ... layout phase ...
resetAfterCommit(container)

Concurrent Mode Complications

What I Learned

Building a custom renderer demystifies React’s “magic” completely. The reconciler is a sophisticated tree diffing algorithm with a well-defined interface. Everything else — hooks, concurrent features, Suspense — builds on top of that interface.

The fiber architecture becomes obvious when you’re on this side: each fiber is a unit of work that can be paused, abandoned, or restarted. The “double buffering” (work-in-progress tree vs. current tree) is why React can show consistent UI while working in the background.

If you want to go deeper, here’s a recommended learning path:

  1. Read the react-reconciler source

    Start with the entry point and trace how the host config methods are invoked. The package is well-organized — each commit phase sub-phase lives in its own file.

  2. Trace a single setState call end-to-end

    Follow one setState from the call site through scheduling, reconciliation, and commit. It’s dense, but it’s one of the most educational things you can do as a React developer.

  3. Study an existing custom renderer

    Ink (terminal renderer) is well-maintained and has clean, readable host config code. Reading it alongside the reconciler source makes both much clearer.

  4. Build your own minimal renderer

    Start with a renderer that targets a plain JavaScript object tree. No display logic — just prove you can mount, update, and unmount components. Add complexity incrementally.

Honey Sharma

Software engineer focused on web engineering, TypeScript, and distributed systems.