refactor node

This commit is contained in:
2026-03-09 14:11:33 +01:00
parent 24efc597b2
commit 9565a425b6
5 changed files with 230 additions and 134 deletions

135
src/lib/abstractNode.ts Normal file
View File

@@ -0,0 +1,135 @@
/**
* Abstract node layer: shared types, hook, and factory for flow node components.
*
* - **AbstractNodeProps<TData>** — Typed props (id, data, width?, height?, selected?) for your node.
* - **useAbstractNode(id, data)** — Flow context plus helpers: nodes, edges, setNodes, setEdges,
* updateData(partial), incomingEdges, outgoingEdges, sourceIds, targetIds.
* - **createAbstractNodeComponent(displayName, Component)** — Wraps with memo + nodePropsAreEqual.
*
* Example: define NodeData type, Props = AbstractNodeProps<NodeData>, use useAbstractNode in the
* component, then export const MyNode = createAbstractNodeComponent('MyNode', MyNodeComponent).
*/
import React, { useCallback, useContext, useMemo } from 'react'
import FlowContext from './flowContext'
import { nodePropsAreEqual } from './flowUtils'
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
/** Props passed by React Flow to custom node components. Extend data with your node's shape. */
export type AbstractNodeProps<TData = Record<string, unknown>> = {
id: string
data: TData
width?: number
height?: number
selected?: boolean
}
/** Edge shape used in flow context (minimal for connection logic). */
export type FlowEdge = { id: string; source: string; target: string; [k: string]: unknown }
/** Node shape used in flow context (minimal for reading graph). */
export type FlowNode = { id: string; type?: string; data?: unknown; position?: { x: number; y: number }; [k: string]: unknown }
/** Result of useAbstractNode: flow context plus helpers scoped to this node. */
export type AbstractNodeContext<TData = Record<string, unknown>> = {
id: string
data: TData
nodes: FlowNode[]
edges: FlowEdge[]
setNodes: (updater: (nodes: FlowNode[]) => FlowNode[]) => void
setEdges: (updater: (edges: FlowEdge[]) => FlowEdge[]) => void
/** Merge partial data into this node's data. Stable reference. */
updateData: (partial: Partial<TData>) => void
/** Incoming edge IDs (edges whose target is this node). */
incomingEdges: FlowEdge[]
/** Outgoing edge IDs (edges whose source is this node). */
outgoingEdges: FlowEdge[]
/** Source node IDs connected to this node (incoming). */
sourceIds: string[]
/** Target node IDs this node connects to (outgoing). */
targetIds: string[]
}
// ---------------------------------------------------------------------------
// Hook
// ---------------------------------------------------------------------------
/**
* Provides flow context and helpers for the current node. Use in any node component
* that receives id and data; updateData(partial) merges into this node's data.
*/
export function useAbstractNode<TData = Record<string, unknown>>(
id: string,
data: TData
): AbstractNodeContext<TData> {
const ctx = useContext(FlowContext)
const nodes = ctx?.nodes ?? []
const edges = ctx?.edges ?? []
const setNodes = ctx?.setNodes
const setEdges = ctx?.setEdges
const updateData = useCallback(
(partial: Partial<TData>) => {
if (!setNodes) return
setNodes((nds: FlowNode[]) =>
nds.map((n) =>
n.id === id ? { ...n, data: { ...(n.data as object), ...partial } } : n
)
)
},
[id, setNodes]
)
const incomingEdges = useMemo(
() => (edges as FlowEdge[]).filter((e) => e.target === id),
[edges, id]
)
const outgoingEdges = useMemo(
() => (edges as FlowEdge[]).filter((e) => e.source === id),
[edges, id]
)
const sourceIds = useMemo(
() => incomingEdges.map((e) => e.source).sort(),
[incomingEdges]
)
const targetIds = useMemo(
() => outgoingEdges.map((e) => e.target).sort(),
[outgoingEdges]
)
return {
id,
data,
nodes,
edges,
setNodes: setNodes ?? (() => {}),
setEdges: setEdges ?? (() => {}),
updateData,
incomingEdges,
outgoingEdges,
sourceIds,
targetIds,
}
}
// ---------------------------------------------------------------------------
// Component factory
// ---------------------------------------------------------------------------
/**
* Wraps a node component with React.memo and nodePropsAreEqual so only id/data/width/height/selected
* changes trigger re-renders. Use with AbstractNodeProps<TData> for typed props.
*/
export function createAbstractNodeComponent<TData = Record<string, unknown>>(
displayName: string,
Component: React.ComponentType<AbstractNodeProps<TData>>
): React.MemoExoticComponent<React.ComponentType<AbstractNodeProps<TData>>> {
const Wrapped = React.memo(Component, nodePropsAreEqual) as React.MemoExoticComponent<
React.ComponentType<AbstractNodeProps<TData>>
>
Wrapped.displayName = displayName
return Wrapped
}