В основе архитектуры React лежит фундаментальное разделение обязанностей между двумя независимыми подсистемами:
Библиотека react-reconciler открывает доступ к ядру Fiber и позволяет инженерам создавать собственные декларативные реактивные рендереры под любые нестандартные среды исполнения:
htop или k9s.Вам не нужно заново писать планировщик задач, алгоритмы обхода очередей или логику хуков useState и useEffect. Реализуя контракт HostConfig из 20+ методов жизненного цикла (createInstance, appendChild, prepareUpdate, commitUpdate), вы поручаете React всё управление состоянием и планированием, фокусируясь только на физическом рисовании узлов на целевой платформе.
Взаимодействие между пользовательским JSX-компонентом, ядром Reconciler и хост-окружением происходит по строгому конвейеру:
┌──────────────────────────────────────────────────────────┐
│ ПОЛЬЗОВАТЕЛЬСКИЙ JSX-КОД │
│ <Stage width={800} height={600}> │
│ <Rectangle x={10} y={20} fill="#3b82f6" /> │
│ <Circle radius={30} fill="#ef4444" /> │
│ </Stage> │
└────────────────────────────┬─────────────────────────────┘
│ React Element Tree (Virtual)
▼
┌──────────────────────────────────────────────────────────┐
│ REACT 19 FIBER RECONCILER │
│ - Concurrent Mode / Transition Scheduler │
│ - Вычисление разницы между current и work-in-progress │
│ - Формирование списка мутаций (Effects / Mutations) │
└────────────────────────────┬─────────────────────────────┘
│ Вызовы методов HostConfig
▼
┌──────────────────────────────────────────────────────────┐
│ КАСТОМНЫЙ HOST CONFIG (Адаптер рендерера) │
│ createInstance('rectangle', props) │
│ prepareUpdate(instance, type, oldProps, newProps) │
│ commitUpdate(instance, updatePayload, type) │
└────────────────────────────┬─────────────────────────────┘
│ Прямые команды платформы
▼
┌──────────────────────────────────────────────────────────┐
│ ЦЕЛЕВОЙ ГРАФ СЦЕНЫ (Host Environment) │
│ HTML5 Canvas 2D Context | CLI Terminal ANSI Canvas │
└──────────────────────────────────────────────────────────┘Контракт HostConfig строго разделяет жизненный цикл на две фазы:
createInstance, createTextInstance, appendInitialChild, prepareUpdate. Выполняются в оперативной памяти над оффлайн-узлами без отображения на экране. Если рендер будет прерван планировщиком (Concurrent interruption) ради более приоритетной задачи, незаконченные структуры просто утилизируются сборщиком мусора (GC) без визуальных артефактов.appendChild, removeChild, commitUpdate, resetAfterCommit. Синхронное применение вычисленных изменений к физическому графу и финальный рендеринг кадра.Определение базовых классов узлов (CanvasNode, Container) с древовидной связью parent/children и свойствами геометрии.
Реализация фабрики createInstance, связывания appendInitialChild и вычисления дельты пропсов prepareUpdate.
Инициализация reconciler.createContainer с ConcurrentRoot-флагом (React 19) и связывание с нативным холстом.
Реализация прохода по дереву фигур в хуке resetAfterCommit и вывод кадра в 2D Context за менее чем 16 мс.
Декларация глобальных типов TypeScript для собственных тегов (<rect>, <circle>) с автодополнением пропсов.
Почему категорически запрещено вызывать команды рисования (например, ctx.fillRect) внутри метода createInstance в HostConfig?
<Candlestick data={bar} />).Перед настройкой Reconciler проектируется древовидная структура узлов сцены в памяти:
export type NodeType = 'stage' | 'rect' | 'circle' | 'text';
export interface BaseNodeProps {
x?: number;
y?: number;
[key: string]: unknown;
}
export interface RectProps extends BaseNodeProps {
width: number;
height: number;
fill: string;
}
export interface CircleProps extends BaseNodeProps {
radius: number;
fill: string;
}
export class CanvasNode {
public type: NodeType;
public props: Record<string, unknown>;
public children: CanvasNode[] = [];
public parent: CanvasNode | null = null;
constructor(type: NodeType, props: Record<string, unknown>) {
this.type = type;
this.props = { ...props };
}
public appendChild(child: CanvasNode): void {
child.parent = this;
this.children.push(child);
}
public removeChild(child: CanvasNode): void {
const index = this.children.indexOf(child);
if (index !== -1) {
child.parent = null;
this.children.splice(index, 1);
}
}
public insertBefore(child: CanvasNode, beforeChild: CanvasNode): void {
const index = this.children.indexOf(beforeChild);
if (index !== -1) {
child.parent = this;
this.children.splice(index, 0, child);
} else {
this.appendChild(child);
}
}
}Конфигурационный объект передается в фабрику ReactReconciler(hostConfig):
import ReactReconciler from 'react-reconciler';
import { DefaultEventPriority } from 'react-reconciler/constants';
import { CanvasNode, type NodeType } from './CanvasNode';
export interface CanvasContainer {
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
rootNode: CanvasNode;
}
export type Instance = CanvasNode;
export type TextInstance = null;
export type SuspenseInstance = null;
export type HydratableInstance = null;
export type PublicInstance = CanvasNode;
export type HostContext = Record<string, unknown>;
export type UpdatePayload = Record<string, unknown> | null;
export const hostConfig: ReactReconciler.HostConfig<
NodeType,
Record<string, unknown>,
CanvasContainer,
Instance,
TextInstance,
SuspenseInstance,
HydratableInstance,
PublicInstance,
HostContext,
UpdatePayload,
unknown,
number,
unknown
> = {
supportsMutation: true,
supportsPersistence: false,
createInstance(type, props) {
return new CanvasNode(type, props);
},
createTextInstance() {
throw new Error('CanvasReconciler: Текстовые узлы должны оборачиваться в компонент <Text>');
},
appendInitialChild(parent, child) {
parent.appendChild(child);
},
appendChild(parent, child) {
parent.appendChild(child);
},
appendChildToContainer(container, child) {
container.rootNode.appendChild(child);
},
removeChild(parent, child) {
parent.removeChild(child);
},
removeChildFromContainer(container, child) {
container.rootNode.removeChild(child);
},
insertBefore(parent, child, beforeChild) {
parent.insertBefore(child, beforeChild);
},
insertInContainerBefore(container, child, beforeChild) {
container.rootNode.insertBefore(child, beforeChild);
},
// Вычисление диффа пропсов на фазе Render
prepareUpdate(instance, type, oldProps, newProps) {
const payload: Record<string, unknown> = {};
let hasChanges = false;
for (const key of Object.keys(newProps)) {
if (newProps[key] !== oldProps[key] && key !== 'children') {
payload[key] = newProps[key];
hasChanges = true;
}
}
return hasChanges ? payload : null;
},
// Атомарное применение диффа на фазе Commit
commitUpdate(instance, updatePayload) {
if (updatePayload) {
Object.assign(instance.props, updatePayload);
}
},
clearContainer(container) {
container.rootNode.children = [];
},
getRootHostContext() {
return {};
},
getChildHostContext(parentHostContext) {
return parentHostContext;
},
shouldSetTextContent() {
return false;
},
finalizeInitialChildren() {
return false;
},
prepareForCommit() {
return null;
},
resetAfterCommit(container) {
// Точка синхронизации: отрисовка кадра на Canvas после применения всех мутаций
redrawCanvas(container);
},
getCurrentEventPriority() {
return DefaultEventPriority;
},
getInstanceFromNode() {
return null;
},
beforeActiveInstanceBlur() {},
afterActiveInstanceBlur() {},
prepareScopeUpdate() {},
getInstanceFromScope() {
return null;
},
detachDeletedInstance() {},
};
function redrawCanvas(container: CanvasContainer): void {
const { ctx, canvas, rootNode } = container;
ctx.clearRect(0, 0, canvas.width, canvas.height);
function renderSubtree(node: CanvasNode): void {
if (node.type === 'rect') {
ctx.fillStyle = (node.props.fill as string) || '#000';
ctx.fillRect(
(node.props.x as number) || 0,
(node.props.y as number) || 0,
(node.props.width as number) || 50,
(node.props.height as number) || 50
);
} else if (node.type === 'circle') {
ctx.fillStyle = (node.props.fill as string) || '#000';
ctx.beginPath();
ctx.arc(
(node.props.x as number) || 0,
(node.props.y as number) || 0,
(node.props.radius as number) || 20,
0,
Math.PI * 2
);
ctx.fill();
}
for (const child of node.children) {
renderSubtree(child);
}
}
for (const child of rootNode.children) {
renderSubtree(child);
}
}Создание FiberRootNode через Reconciler с привязкой к нативному 2D Context HTML5 Canvas.
Fiber строит оффлайн-дерево CanvasNode в памяти и рассчитывает diff пропсов (x, y, fill).
Reconciler передает вычисленный diff в граф узлов сцены без блокировки DOM браузера.
Хук завершения коммита очищает холст и выполняет рекурсивный проход по графу фигур.
Что вернет метод prepareUpdate, если у компонента <Rectangle width={100} fill='blue' /> изменился только цвет заливки на fill='red'?
Ниже представлена готовая точка входа кастомного Canvas-рендерера CanvasRenderer, реализующая API инициализации контейнера createRoot на базе React 19 Concurrent Root:
import React from 'react';
import ReactReconciler from 'react-reconciler';
import { hostConfig, type CanvasContainer } from './hostConfig';
import { CanvasNode } from './CanvasNode';
const reconciler = ReactReconciler(hostConfig);
/**
* Рендерер для рисования React-компонентов непосредственно на HTML5 Canvas
*/
export class CanvasRenderer {
private container: CanvasContainer;
private fiberRoot: ReactReconciler.FiberRoot;
private isMounted: boolean = true;
constructor(canvas: HTMLCanvasElement) {
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('[CanvasRenderer]: Не удалось получить CanvasRenderingContext2D');
}
this.container = {
canvas,
ctx,
rootNode: new CanvasNode('stage', {}),
};
// 0 = LegacyRoot, 1 = ConcurrentRoot (стандарт React 19)
const ConcurrentRootTag = 1;
this.fiberRoot = reconciler.createContainer(
this.container,
ConcurrentRootTag,
null,
false,
null,
'',
(error: Error) => {
console.error('[Canvas Reconciler Fatal Error]:', error);
},
null
);
}
/**
* Монтирование или обновление JSX-дерева в Canvas контейнере
*/
public render(element: React.ReactNode): void {
if (!this.isMounted) {
throw new Error('[CanvasRenderer]: Нельзя вызвать render на размонтированном контейнере');
}
reconciler.updateContainer(element, this.fiberRoot, null, () => {
// Опциональный коллбэк завершения первичной гидратации дерева
});
}
/**
* Корректное размонтирование дерева с вызовом всех useEffect/useLayoutEffect cleanup
*/
public unmount(): void {
if (!this.isMounted) return;
reconciler.updateContainer(null, this.fiberRoot, null, () => {
hostConfig.clearContainer(this.container);
this.container.ctx.clearRect(
0,
0,
this.container.canvas.width,
this.container.canvas.height
);
this.isMounted = false;
});
}
}Вызов команд рисования (ctx.fillRect(...)) внутри createInstance или prepareUpdate. В Concurrent Mode фаза Render может быть прервана или перезапущена. Это приведет к отрисовке фантомных фигур, которых нет в финальном дереве компонентов.
// ❌ Антипаттерн: побочный эффект в фазе Render!
createInstance(type, props) {
ctx.fillRect(props.x, props.y, props.w, props.h); // Рисует до фиксации коммита!
return new CanvasNode(type, props);
}
// ✅ Правильное решение: только создание структур данных в памяти.
// Отрисовка выполняется строго в хуке resetAfterCommit после коммита.Если prepareUpdate возвращает true или пустой объект вместо конкретного словаря изменившихся свойств, commitUpdate не сможет определить, какие именно параметры изменились, что нивелирует все преимущества гранулярного диффинга.
// ❌ Антипаттерн: потеря детализации изменений
prepareUpdate() {
return true; // commitUpdate не знает, ЧТО именно изменилось
}
// ✅ Правильное решение: возврат четкого словаря мутаций
prepareUpdate(instance, type, oldProps, newProps) {
const diff: Record<string, unknown> = {};
if (oldProps.fill !== newProps.fill) diff.fill = newProps.fill;
return Object.keys(diff).length > 0 ? diff : null;
}Элементы внутри Canvas (например, <Circle /> или <Rectangle />) являются узлами виртуальной сцены CanvasNode, а не DOM-узлами. Они не поддерживают нативный addEventListener.
Решение: повесить единый слушатель кликов на внешний HTML-элемент <canvas>, считывать координаты event.offsetX, event.offsetY и выполнять математический Hit Testing по графу фигур в обратном порядке Z-index.
При удалении узла в removeChild необходимо явно занулять ссылку child.parent = null. В противном случае циклические ссылки между узлами графа сцены и замыканиями обработчиков событий будут удерживаться в памяти JS Heap даже после размонтирования компонента.
react-reconciler позволяет реализовать интерфейс HostConfig и создавать рендереры под Canvas, WebGL, Terminal (CLI) и PDF.createInstance, appendInitialChild и prepareUpdate работают в фазе Render и не должны содержать побочных эффектов или прямого рисования.resetAfterCommit.