React Reconciler for Custom Renderers
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
-
Error: This module must be shimmed by a specific renderer.
cause Attempting to import internal `ReactFiberConfig` directly or a similar internal React module that expects a renderer-specific shim.fixYou should not be directly importing internal React modules like `ReactFiberConfig`. Instead, you define a `HostConfig` object and pass it to the `Reconciler` factory function. -
Warning: Each child in a list should have a unique 'key' prop.
cause While a general React warning, it's critical for custom renderers as it directly impacts the reconciliation algorithm's performance when diffing lists of elements. Using array indices as keys for dynamic lists is a common cause.fixAlways provide a stable, unique `key` prop for each item in a list. This key should ideally come from the data itself, not the array index, especially if the list can be reordered, filtered, or grow. -
UI doesn't update or renders inconsistently after state changes.
cause In a custom renderer, this often means `updateContainer` is not called correctly after state changes, or the `HostConfig` methods (e.g., `commitUpdate`, `commitTextUpdate`, `appendChild`, `removeChild`) are not implemented to correctly apply changes to the host environment.fixVerify that `MyRenderer.updateContainer(element, root, null, callback)` is invoked whenever the root element changes. Debug your `HostConfig` methods to ensure they correctly translate React's instructions into operations on your host environment's instances.
Warnings
- breaking The `react-reconciler` package is explicitly marked as experimental and unstable. Its API does not follow React's usual semantic versioning, and breaking changes can occur without major version bumps. Users should expect to adapt their custom renderers frequently.
- gotcha Implementing a complete and performant `HostConfig` is a highly complex task, requiring deep understanding of React's Fiber architecture and the target platform's rendering specifics. Missing or incorrectly implemented methods can lead to subtle bugs, memory leaks, or poor performance.
- gotcha Choosing between `supportsMutation: true` and `supportsPersistence: true` in your `HostConfig` is critical. Incorrectly selecting the mode (e.g., mutation for an immutable tree) will lead to incorrect rendering, performance issues, and potential crashes.
- gotcha The peer dependency `react` must match the expected major version for `react-reconciler`. For version 0.33.0, this is `^19.2.0`. Mismatched React versions can lead to runtime errors or unexpected behavior due to internal API differences.
Install
-
npm install react-reconciler -
yarn add react-reconciler -
pnpm add react-reconciler
Imports
- createReconciler
import { createReconciler } from 'react-reconciler';import Reconciler from 'react-reconciler';
- DiscreteEventPriority, ContinuousEventPriority, DefaultEventPriority
import { DefaultEventPriority } from 'react-reconciler';import { DiscreteEventPriority, ContinuousEventPriority, DefaultEventPriority } from 'react-reconciler/constants'; - HostConfig (interface/type)
import { HostConfig } from 'react-reconciler';import type { HostConfig } from 'react-reconciler';
Quickstart
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);