Portal Community

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
    />
  );
}
Declare nodeTypes Outside the Component If you declare the 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:

  1. Create the renderer — extend BaseNode and wrap with withNodeHooks
  2. Import it into WorkflowCanvas.tsx
  3. Add an entry to the nodeTypes map 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
};
Double-Wrapping If you call 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.

LookupValue
WorkflowNode.type"my-custom-node" (from DB NodeType.typeCode)
nodeTypes["my-custom-node"]MyCustomNode (your renderer)
Fallback if not foundReact 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.

ScenarioRegistration KeyRenderer
Standard node using rectangle shape"rectangle"CustomNode (built-in)
Custom node with unique visual"my-custom-node" (typeCode)MyCustomNode (yours)