Registering in Canvas
React Flow's nodeTypes prop is the central registry that maps a typeCode string to a renderer component. Every custom node must be registered here — otherwise React Flow renders a default placeholder.
Where Registration Lives
File: packages/flow-studio-designer/src/components/Canvas/WorkflowCanvas.tsx
The WorkflowCanvas component owns the nodeTypes map. It is passed directly to the React Flow <ReactFlow> component. React Flow memoizes this object — it must be declared outside the component function (or wrapped in useMemo with no deps) to avoid re-rendering all nodes on every render cycle.
The nodeTypes Map
// packages/flow-studio-designer/src/components/Canvas/WorkflowCanvas.tsx
import { withNodeHooks } from '../Nodes/Base/withNodeHooks';
import { CustomNode } from '../Nodes/CustomNode';
import { CircleNode } from '../Nodes/CircleNode';
import { DiamondNode } from '../Nodes/DiamondNode';
import { AIAgentNode } from '../Nodes/AIAgentNode';
// --- Built-in renderers ---
const nodeTypes = {
// Shape-based built-ins
rectangle: withNodeHooks(CustomNode),
circle: withNodeHooks(CircleNode),
diamond: withNodeHooks(DiamondNode),
ai-agent: withNodeHooks(AIAgentNode),
// Specific typeCode overrides (take priority over shape)
'my-custom-node': withNodeHooks(MyCustomNode),
};
export function WorkflowCanvas() {
return (
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes} // <-- registered here
/>
);
}
nodeTypes object inside the component function body, React creates a new object reference on every render. React Flow interprets this as "all node types changed" and re-mounts every node on the canvas. Always declare it outside the function or use useMemo with an empty dependency array.
Adding Your Custom Node
Three steps:
- Create the renderer — extend
BaseNodeand wrap withwithNodeHooks - Import it into
WorkflowCanvas.tsx - Add an entry to the
nodeTypesmap using the exact typeCode from the NodeType DB record
// Step 1 — MyCustomNode.tsx
import { BaseNode, BaseNodeProps } from '../Base/BaseNode';
import { withNodeHooks } from '../Base/withNodeHooks';
class MyCustomNodeClass extends BaseNode {
renderBody() { ... }
}
export const MyCustomNode = withNodeHooks(MyCustomNodeClass);
// Step 2 + 3 — WorkflowCanvas.tsx
import { MyCustomNode } from '../Nodes/MyCustomNode';
const nodeTypes = {
// ... existing entries ...
'my-custom-node': MyCustomNode, // already wrapped — do not wrap again
};
withNodeHooks(MyCustomNode) in the nodeTypes map AND also export it pre-wrapped from the renderer file, you will inject the stores twice. The convention is: always pre-wrap at the export site and import the already-wrapped component into the canvas.
TypeCode Resolution Order
When the canvas renders a node, it resolves the renderer by first checking nodeTypes for the node's type value. This value comes from the WorkflowNode's type field, which is set to the NodeType's typeCode by ProcessElementToNodeTranslator.
| Lookup | Value |
|---|---|
| WorkflowNode.type | "my-custom-node" (from DB NodeType.typeCode) |
| nodeTypes["my-custom-node"] | MyCustomNode (your renderer) |
| Fallback if not found | React Flow default (grey box — not usable in prod) |
Shape vs TypeCode Registration
Built-in node shapes (rectangle, circle, diamond) are registered by shape name. If a NodeType sets shape: "rectangle", the canvas uses the CustomNode renderer automatically — no custom registration needed. A typeCode-specific registration takes priority when you need a fully custom visual. Both can coexist.
| Scenario | Registration Key | Renderer |
|---|---|---|
| Standard node using rectangle shape | "rectangle" | CustomNode (built-in) |
| Custom node with unique visual | "my-custom-node" (typeCode) | MyCustomNode (yours) |