# SkillPatch skill: react-flow-node-ts

This skill guides agents in creating custom React Flow node components using TypeScript, complete with proper type definitions, handle configurations, and Zustand store integration. It provides copy-paste templates, naming conventions, code patterns for both node components and type definitions, and a step-by-step integration checklist for wiring nodes into a React Flow canvas application.

You (the agent) were given this URL and asked to use this skill. This is a **public** skill — no SkillPatch account, API key, or prior setup is required. Two ways to use it:

**1. Use it right now** — the skill's complete file tree (SKILL.md + all reference files) is inlined below; read `SKILL.md` first, then follow it, consulting the other files as it directs.

**2. Install the exact package onto disk** (recommended if you can run a shell — this reproduces the skill byte-for-byte, including any binary assets that can't be inlined):

```bash
mkdir -p .claude/skills/react-flow-node-ts
curl -sSL https://skillpatch.dev/install_skill/react-flow-node-ts | tar -xz -C .claude/skills/
```

(`.claude/skills/` is Claude Code's convention; use whatever directory your agent loads skills from.)


---

## Skill files (3)

- `SKILL.md`
- `assets/template.tsx`
- `assets/types.template.ts`


### `SKILL.md`

````markdown
---
name: react-flow-node-ts
description: Create React Flow node components with TypeScript types, handles, and Zustand integration. Use when building custom nodes for React Flow canvas, creating visual workflow editors, or implementing node-based UI components.
license: MIT
metadata:
  author: Microsoft
  version: "1.0.0"
---

# React Flow Node

Create React Flow node components following established patterns with proper TypeScript types and store integration.

## Quick Start

Copy templates from [assets/](assets/) and replace placeholders:
- `{{NodeName}}` → PascalCase component name (e.g., `VideoNode`)
- `{{nodeType}}` → kebab-case type identifier (e.g., `video-node`)
- `{{NodeData}}` → Data interface name (e.g., `VideoNodeData`)

## Templates

- [assets/template.tsx](assets/template.tsx) - Node component
- [assets/types.template.ts](assets/types.template.ts) - TypeScript definitions

## Node Component Pattern

```tsx
export const MyNode = memo(function MyNode({
  id,
  data,
  selected,
  width,
  height,
}: MyNodeProps) {
  const updateNode = useAppStore((state) => state.updateNode);
  const canvasMode = useAppStore((state) => state.canvasMode);
  
  return (
    <>
      <NodeResizer isVisible={selected && canvasMode === 'editing'} />
      <div className="node-container">
        <Handle type="target" position={Position.Top} />
        {/* Node content */}
        <Handle type="source" position={Position.Bottom} />
      </div>
    </>
  );
});
```

## Type Definition Pattern

```typescript
export interface MyNodeData extends Record<string, unknown> {
  title: string;
  description?: string;
}

export type MyNode = Node<MyNodeData, 'my-node'>;
```

## Integration Steps

1. Add type to `src/frontend/src/types/index.ts`
2. Create component in `src/frontend/src/components/nodes/`
3. Export from `src/frontend/src/components/nodes/index.ts`
4. Add defaults in `src/frontend/src/store/app-store.ts`
5. Register in canvas `nodeTypes`
6. Add to AddBlockMenu and ConnectMenu

````


### `assets/template.tsx`

```
import { memo, useCallback } from 'react';
import { NodeProps, Node, Handle, Position, NodeResizer } from '@xyflow/react';
import { {{NodeData}} } from '@/types';
import { useAppStore } from '@/store';
import { cn } from '@/utils/utils';

type {{NodeName}}Props = NodeProps<Node<{{NodeData}}>>;

/**
 * {{NodeName}} - Description of what this node does
 * 
 * @example
 * // Node data structure
 * const data: {{NodeData}} = {
 *   title: 'Example',
 *   // ... other properties
 * };
 */
export const {{NodeName}} = memo(function {{NodeName}}({
  id,
  data,
  selected,
  width,
  height,
}: {{NodeName}}Props) {
  // Store selectors - use individual selectors for performance
  const updateNode = useAppStore((state) => state.updateNode);
  const canvasMode = useAppStore((state) => state.canvasMode);
  const isEditing = canvasMode === 'editing';

  // Handlers
  const handleTitleChange = useCallback(
    (title: string) => {
      updateNode(id, { title });
    },
    [id, updateNode]
  );

  return (
    <>
      {/* NodeResizer - only visible in editing mode */}
      {isEditing && (
        <NodeResizer
          minWidth={200}
          minHeight={150}
          isVisible={selected}
          lineClassName="!border-[var(--frontier-primary)]"
          handleClassName="!w-2 !h-2 !bg-[var(--frontier-primary)] !border-none"
        />
      )}

      {/* Node Container */}
      <div
        className={cn(
          'bg-[var(--frontier-surface)]',
          'border-2 rounded-xl overflow-hidden',
          'transition-all duration-200',
          selected
            ? 'border-[var(--frontier-primary)] shadow-lg shadow-[var(--frontier-primary)]/20'
            : 'border-[var(--frontier-border)]'
        )}
        style={{ width: width ?? 280, height: height ?? 'auto' }}
      >
        {/* Target Handle (top) */}
        <Handle
          type="target"
          position={Position.Top}
          className="!w-3 !h-3 !bg-[var(--frontier-primary)] !border-2 !border-[var(--frontier-surface)]"
        />

        {/* Node Header */}
        <div className="px-4 py-3 border-b border-[var(--frontier-border)]">
          {isEditing ? (
            <input
              type="text"
              value={data.title}
              onChange={(e) => handleTitleChange(e.target.value)}
              className={cn(
                'w-full bg-transparent text-[var(--frontier-text)]',
                'font-semibold text-sm outline-none',
                'focus:ring-1 focus:ring-[var(--frontier-primary)] rounded px-1'
              )}
            />
          ) : (
            <h3 className="text-[var(--frontier-text)] font-semibold text-sm truncate">
              {data.title}
            </h3>
          )}
        </div>

        {/* Node Content */}
        <div className="p-4">
          {/* Add node-specific content here */}
          <p className="text-[var(--frontier-text-muted)] text-xs">
            Node content goes here
          </p>
        </div>

        {/* Source Handle (bottom) */}
        <Handle
          type="source"
          position={Position.Bottom}
          className="!w-3 !h-3 !bg-[var(--frontier-primary)] !border-2 !border-[var(--frontier-surface)]"
        />
      </div>
    </>
  );
});

```


### `assets/types.template.ts`

````
import { Node } from '@xyflow/react';

/**
 * {{NodeData}} - Data structure for {{NodeName}}
 * 
 * Extends Record<string, unknown> for React Flow compatibility.
 */
export interface {{NodeData}} extends Record<string, unknown> {
  /** Display title for the node */
  title: string;
  
  /** Optional description */
  description?: string;
  
  // Add additional properties specific to this node type
}

/**
 * {{NodeName}} type alias for React Flow
 * 
 * Usage:
 * ```typescript
 * const node: {{NodeName}} = {
 *   id: 'unique-id',
 *   type: '{{nodeType}}',
 *   position: { x: 0, y: 0 },
 *   data: { title: 'Example' },
 * };
 * ```
 */
export type {{NodeName}} = Node<{{NodeData}}, '{{nodeType}}'>;

// Remember to add {{NodeName}} to the AppNode union type in types/index.ts:
// export type AppNode = VideoNode | ImageNode | ... | {{NodeName}};

````
