Перейти к содержимому

Миграция с React Flow

У вас приложение на React Flow, а нужны WebGL-канвас, типизированные пины или MCP-поверхность. Две части: механически импортировать граф (один вызов функции) и перевести ментальную модель (здесь библиотеки действительно различаются). Страница покрывает обе части — включая честный список того, что импортёр унести не может.

Импорт графа

Вход — вывод React Flow toObject(); зависимости reactflow не нужно (импортёр без зависимостей, принимает простой JSON):

import { importFromReactFlow } from '@xenolithengine/graph-editor'
const rfJson = rfInstance.toObject() // { nodes, edges, viewport }
const { doc, report } = importFromReactFlow(rfJson, {
// опционально: дать синтезированным пинам реальные типы
inferType: ({ nodeType, handle }) =>
nodeType === 'number' ? 'float' : undefined, // undefined → wildcard 'any'
// опционально: ноды с типом из schemas используют ЕЁ пины
schemas: [myNodeSchemas],
})
editor.loadJSON(doc) // или одной строкой: const report = editor.importReactFlow(rfJson, opts)

Маппинг почти 1:1 там, где модели совпадают:

React Flowxenolith.v1
node.id / node.type / node.positionкак есть (нет типа → 'default')
node.datanode.state как есть; data.label дополнительно становится заголовком ноды
edge.id / source / target / handlesкак есть (недостающие id рёбер синтезируются)
edge.type: default/simplebezier → bezier, straight → linear, step, smoothstepedge.opts.pathStyle
edge.animated, edge.label, стрелка markerEndedge.opts.animated / .label / .markerEnd
viewportviewport
Handles, которых касаются рёбрапины — по одному на уникальный (node, handle, direction); label = handle id, multiple: true, тип из inferType или 'any'
Handle у ноды с совпадающим типом в schemas[]типизированные пины схемы; handle резолвится по label (без регистра), числовому индексу или id пина

Отчёт о потерях

Ничто не выбрасывается молча — импортёр возвращает полный отчёт:

interface ImportReport {
counts: { nodes: number; edges: number; pins: number }
unknownNodes: string[] // endpoints рёбер на несуществующие ноды (ребро отброшено)
unknownHandles: { edgeId, side, nodeId, handle }[] // handle не нашёл пин схемы (ребро отброшено)
droppedFields: Record<string, number> // 'node.width': 14, 'edge.style': 3, …
warnings: string[] // напр. subflow-parenting, синтезированные 'any'-пины
}

Полный список того, что не переносится:

  • Subflow-иерархия (parentId / group-ноды). Дети импортируются как ноды верхнего уровня — сгруппируйте их макросами после импорта (editor.createMacroFromSelection).
  • Измеренная RF-геометрия (node.width / node.height, sourcePosition / targetPosition). Xenolith считает размер ноды из схемы; объявленная геометрия пинов — одна из причин, почему он быстр на 1000+ нод.
  • Состояние представления (selected, dragging, hidden, style, className, zIndex, …) и стилизация рёбер сверх path style/label/стрелки (edge.style, markerStart, цвета маркеров).
  • Handles, которых не касается ни одно ребро — JSON React Flow вообще не объявляет handles (они живут в ваших компонентах), поэтому пины синтезируются только из того, что используют рёбра. Передайте schemas[] для полной картины.
  • Кастомные типы рёбер — всё вне default/simplebezier/straight/step/smoothstep считается в edge.type и падает обратно в bezier.

Перевод ментальной модели

Рендерер и модель состояния — вот где библиотеки действительно различаются.

Концепция React FlowЭквивалент XenolithGraph
<ReactFlow nodes={} edges={}> (controlled-пропсы)Документ живёт в редакторе. Чтение — editor.toJSON() / useGraphJSON(). Полная замена — editor.loadJSON(doc), точечная запись — editor.applyChanges(changes) (один шаг undo). graph:changed отдаёт склеенный массив изменений на коммите транзакции, undo-группы или undo/redo — никогда на кадр перетаскивания.
useNodesState / applyNodeChangesuseNodesState() в @xenolithengine/graph-react и @xenolithengine/graph-vue возвращает { nodes, edges, applyChanges, setNodes, setEdges }. setNodes(next) и setEdges(next) диффят граф одним шагом undo каждый. В Svelte и Solid та же поверхность называется nodesState(), в Angular она на XenolithGraphService. Выделение — не поле узла: читай useSelection() / selection:changed. useNodes / useEdges / useViewport остаются подписками только на чтение.
<Handle type="source"> в JSXТипизированные пины, объявленные один раз в NodeSchema ({ direction: 'out', type: 'float', label: 'Out' }). Соединения проверяют типы; any — явный wildcard.
nodeTypes + кастомный компонент нодыСхема + DOM-виджеты (обёртки reactWidget, freeFloating-контролы в теле, сайдбар). Тело ноды рисует рендерер — это и есть WebGL-ров; ваш HTML живёт в виджетах.
node.datanode.state (значения виджетов); импортёр переносит как есть.
onConnect → addEdgeeditor.connect({ source, sourceHandle: 'Out', target, targetHandle: 'In' }) — узел или id, те же селекторы пина, что у позиционной формы (id, label, индекс). null в handle значит единственный пин этого направления. Undoable, проверка типов. setEdges(eds => [...eds, edge]) — форма через массив.
isValidConnectioneditor.setIsValidConnection(predicate) — тот же хук плюс встроенная совместимость типов.
onNodesChange (drag-обновления)Ничего на кадр перетаскивания — позиции принадлежат рендереру, пока жест не закоммичен. Коммит приходит как node:moved и как одна запись в массиве graph:changed.
<Background>, <Controls>, <MiniMap>Встроено: editor.chrome.setControls(), миникарта, сетка — монтировать компоненты не нужно.
Темизация через CSSТокены дизайна (Xen, Daylight, Liquid Glass) — цвет, геометрия и типографика как данные.
Тестирование с @testing-library + jsdom@xenolithengine/graph-test-utils — реальный редактор поднимается в jsdom.

Самое глубокое различие: React Flow перерисовывает дерево компонентов на каждом кадре перетаскивания; у XenolithGraph рендерер владеет канвасом, DOM неподвижен. React остаётся для панелей, тулбаров и состояния приложения — поверхность редактора императивная и событийная, как map или editor instance, а не дерево пропсов.

После импорта

const report = editor.importReactFlow(rfJson, { schemas: mySchemas })
if (report.unknownNodes.length) console.warn('отброшенные рёбра →', report.unknownNodes)
editor.view.fitView({ padding: 80 }) // вписать импортированный лейаут
editor.history.clear() // чтобы первый Ctrl+Z не разматывал импорт

Зарегистрируйте схемы своих типов нод (палитра, виджеты, реальные типы пинов) и переимпортируйте с schemas — или постобработайте doc перед loadJSON. Приложение продолжает работать с тем же JSON, который вы версионировали для React Flow.