В основі архітектури 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.