React Reconciler for Custom Renderers

0.33.0 · active · verified Wed Apr 22

react-reconciler is an experimental, low-level package provided by the React team for developers who wish to build custom React renderers for non-standard environments, such as WebGL, terminal UIs, or native desktop applications. It encapsulates the core reconciliation logic (the 'Fiber' architecture since React 16) responsible for efficiently diffing component trees and determining the minimal set of changes needed to update the UI. The package, currently at version 0.33.0, is designed to be pluggable with a custom `HostConfig` object, allowing developers to define platform-specific operations for creating, updating, and deleting nodes. Its API is explicitly unstable and does not adhere to React's standard versioning scheme, meaning breaking changes can occur in minor versions. It requires `react` as a peer dependency, specifically `>=19.2.0` for this version. It supports two primary modes: 'mutation mode' for environments that modify existing nodes (like the DOM) and 'persistent mode' for immutable tree structures.

Common errors

Warnings

Install

Imports

Quickstart

This example demonstrates how to create a basic custom React renderer using `react-reconciler`. It defines a minimal `HostConfig` for a 'console' environment, logs lifecycle events, and shows how to use `createContainer` and `updateContainer` to render a simple React component.

import React from 'react';
import Reconciler from 'react-reconciler';

// Minimal HostConfig for a 'console' renderer
const hostConfig = {
  now: Date.now,
  supportsMutation: true, // Or supportsPersistence: true for immutable trees
  getRootHostContext: () => ({}),
  getChildHostContext: (parentHostContext, type) => parentHostContext,
  getPublicInstance: (instance) => instance,
  prepareForCommit: () => null,
  resetAfterCommit: () => {},

  createInstance: (type, props, rootContainerInstance, hostContext, internalInstanceHandle) => {
    // In a real renderer, this would create a DOM node, canvas object, etc.
    const instance = { type, props, children: [] };
    console.log(`CREATE: <${type}>`, props);
    return instance;
  },
  createTextInstance: (text, rootContainerInstance, hostContext, internalInstanceHandle) => {
    console.log(`CREATE TEXT: "${text}"`);
    return text;
  },
  appendInitialChild: (parentInstance, child) => {
    parentInstance.children.push(child);
    console.log(`APPEND INITIAL CHILD:`, { parent: parentInstance.type, child });
  },
  appendChild: (parentInstance, child) => {
    parentInstance.children.push(child);
    console.log(`APPEND CHILD:`, { parent: parentInstance.type, child });
  },
  insertBefore: (parentInstance, child, beforeChild) => {
    const index = parentInstance.children.indexOf(beforeChild);
    if (index > -1) {
      parentInstance.children.splice(index, 0, child);
    }
    console.log(`INSERT BEFORE:`, { parent: parentInstance.type, child, before: beforeChild });
  },
  removeChild: (parentInstance, child) => {
    parentInstance.children = parentInstance.children.filter(c => c !== child);
    console.log(`REMOVE CHILD:`, { parent: parentInstance.type, child });
  },
  prepareUpdate: (instance, type, oldProps, newProps, rootContainerInstance, hostContext) => {
    // Compare oldProps and newProps to determine what needs to change.
    const payload = {};
    for (const key in newProps) {
      if (newProps[key] !== oldProps[key]) {
        payload[key] = newProps[key];
      }
    }
    return Object.keys(payload).length > 0 ? payload : null;
  },
  commitUpdate: (instance, updatePayload, type, oldProps, newProps, internalHandle) => {
    Object.assign(instance.props, updatePayload);
    console.log(`UPDATE: <${type}>`, updatePayload);
  },
  commitTextUpdate: (textInstance, oldText, newText) => {
    console.log(`TEXT UPDATE: "${oldText}" -> "${newText}"`);
    Object.assign(textInstance, newText); // Assuming textInstance is mutable string or wrapper
  },
  shouldSetTextContent: (type, props) => typeof props.children === 'string' || typeof props.children === 'number',
  // Add other required methods like scheduleTimeout, cancelTimeout, noTimeout, etc.
  scheduleTimeout: setTimeout,
  cancelTimeout: clearTimeout,
  noTimeout: -1,
  isPrimaryRenderer: true,
  getCurrentEventPriority: () => 3, // DefaultEventPriority (requires import from 'react-reconciler/constants')
  // A minimal set of methods is still quite large. Many can be no-ops initially.
};

const MyRenderer = Reconciler(hostConfig);

const rootContainer = {}; // Represents the 'host' root, e.g., a canvas element, a terminal object
const root = MyRenderer.createContainer(rootContainer, false, false); // No hydrate, no hydrationCallbacks

export function render(element, container) {
  // `container` here would be the root object in your host environment
  // like `document.getElementById('root')` for ReactDOM.
  // For this console example, we'll just use the `rootContainer` created above.
  MyRenderer.updateContainer(element, root, null, () => {
    console.log('RENDER COMPLETE!', rootContainer);
    // For a console renderer, you'd print the final tree here.
    // In a real app, this would trigger a flush of pending updates to the actual UI.
  });
}

// Example usage
function App() {
  const [count, setCount] = React.useState(0);
  React.useEffect(() => {
    const interval = setInterval(() => setCount(c => c + 1), 1000);
    return () => clearInterval(interval);
  }, []);

  return (
    <box color="blue">
      <text>{`Count: ${count}`}</text>
      <button onClick={() => setCount(0)}>Reset</button>
    </box>
  );
}

render(<App />, rootContainer);

view raw JSON →