Connections
Let people drag wires between sockets, turn a finished drag into an edge, and reject the connections your graph can't use.
People connect two entities by dragging a wire from one socket to another. When they let go
over a socket that accepts the wire, the canvas calls onConnect, and your code adds the edge.
You set which wires are allowed with connectionMode, isValidConnection and allowCycles.
Drag a wire between sockets
Press on any socket and drag. A wire can start from an output or from an input, and it follows the pointer until you let go.
While you drag, the canvas shows where the wire can go:
- The wire has the colour of the socket type it started from.
- Every socket on another entity that faces the other way and has a compatible type shows a halo
in the success colour,
--success-9. - When the socket under the pointer faces the same way, sits on the same entity or has an
incompatible type, that socket and the wire turn the destructive colour,
--destructive-9. - When
allowCyclesisfalse, the wire also turns the destructive colour over a socket that would close a loop. - Near the edge of the canvas, within 50 pixels, the view pans to follow the pointer, so you can reach an entity that is off screen.
Press Escape to drop the wire without connecting it.
These colours don't reflect connectionMode or your own isValidConnection. In the default
loose mode, a wire that turned the destructive colour only because its types don't match still
connects when you let go. A wire that your own rule rejects can look acceptable until you let go.
Create the edge
The canvas doesn't add an edge by itself. When a wire is accepted, it calls onConnect with a
Connection, and you add the edge to the edges you pass back. Without an onConnect, the
wire disappears when you let go.
| Field | Type | Meaning |
|---|---|---|
source | string | null | The entity on the output side. |
sourceSocket | string | null | The output socket. |
target | string | null | The entity on the input side. |
targetSocket | string | null | The input socket. |
invalid | boolean | true when the two socket types aren't compatible. |
source is always the output side, whichever end the drag started from. The canvas fills all
four ids when it calls onConnect. They're typed as nullable, so check them before you use
them as strings.
invalid reports type compatibility and nothing else. It is true for an incompatible wire
that loose mode accepted, and also for one that your own isValidConnection accepted. An edge
that keeps invalid: true draws as a dashed line in the destructive colour. The
Edges chapter explains how to set it yourself.
useGraph returns an onConnect that appends an edge with the id
`${source}-${sourceSocket}-${target}-${targetSocket}` and copies invalid onto it. It
doesn't look for an edge that is already there. An input can take several wires, and dragging
the same wire twice adds a second edge with the same id.
Write your own handler when an input should hold one wire. This one replaces whatever was
plugged into the input, and reports the change through onEdgesChange so useGraph records it
as one undo step when history is on:
editor.tsximport { KookieFlow, useGraph, type Connection, type EdgeChange, type Entity } from '@kushagradhawan/kookie-flow';export function Editor({ initialEntities }: { initialEntities: Entity[] }) { const { entities, edges, onEntitiesChange, onEdgesChange } = useGraph({ initialEntities }); function handleConnect({ source, sourceSocket, target, targetSocket, invalid }: Connection) { if (!source || !sourceSocket || !target || !targetSocket) return; // One wire per input: unplug whatever was in this one before. const changes: EdgeChange[] = edges .filter((edge) => edge.target === target && edge.targetSocket === targetSocket) .map((edge): EdgeChange => ({ type: 'remove', id: edge.id })); const id = `${source}.${sourceSocket}->${target}.${targetSocket}`; changes.push({ type: 'add', edge: { id, source, sourceSocket, target, targetSocket, invalid } }); onEdgesChange(changes); } return ( <KookieFlow entities={entities} edges={edges} onEntitiesChange={onEntitiesChange} onEdgesChange={onEdgesChange} onConnect={handleConnect} /> );}Choose loose or strict connections
connectionMode sets how strictly the canvas checks socket types when you let go of a wire.
'loose'is the default. It accepts a wire between any output and any input on two different entities. An incompatible wire still connects, withinvalid: true.'strict'also rejects a wire whose socket types aren't compatible.
Both modes reject a wire from an input to an input, from an output to an output, and from an
entity to itself. Types are compatible when they're the same, when either is any, or when
either type's compatibleWith lists the other or is '*'. The
Sockets and widgets chapter covers those rules.
Arrow keys move between nodes. Press Enter to reach the focused node's controls, and Escape to come back here.
'use client';import { useEffect, useRef } from 'react';import { KookieFlow, useGraph, type Entity, type KookieFlowInstance } from '@kushagradhawan/kookie-flow';// A float output, an image output, and one node that takes one of each.const initialEntities: Entity[] = [ { id: 'number', type: 'default', position: { x: 0, y: 0 }, data: { label: 'Number' }, outputs: [{ id: 'value', name: 'Value', type: 'float' }], }, { id: 'photo', type: 'default', position: { x: 0, y: 160 }, data: { label: 'Photo' }, outputs: [{ id: 'image', name: 'Image', type: 'image' }], }, { id: 'blur', type: 'default', position: { x: 400, y: 60 }, data: { label: 'Blur' }, inputs: [ { id: 'image', name: 'Image', type: 'image' }, { id: 'radius', name: 'Radius', type: 'float' }, ], },];export default function ConnectionRulesExample() { const { entities, edges, onEntitiesChange, onEdgesChange, onConnect } = useGraph({ initialEntities, }); const flowRef = useRef<KookieFlowInstance>(null); useEffect(() => { flowRef.current?.fitView({ padding: 60, maxZoom: 1 }); }, []); return ( <div style={{ width: '100%', height: 360 }}> {/* Strict: a wire only connects sockets whose types are compatible. */} <KookieFlow ref={flowRef} entities={entities} edges={edges} onEntitiesChange={onEntitiesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} connectionMode="strict" /> </div> );}This graph uses connectionMode="strict". Drag from Value to the Image input on
Blur: the wire turns the destructive colour and disappears when you let go, because a float
can't feed an image. Drag from Value to Radius instead, and the two float sockets
connect.
| Prop | Type | Default | Meaning |
|---|---|---|---|
onConnect | (connection: Connection) => void | none | Called when a wire is accepted. |
onConnectStart | (event: PointerEvent, params: OnConnectStartParams) => void | none | Called when the pointer goes down on a socket. |
onConnectEnd | (event: PointerEvent, state: ConnectionEndState) => void | none | Called when a wire is let go, accepted or not. |
connectionMode | ConnectionMode | 'loose' | 'loose' or 'strict'. |
isValidConnection | IsValidConnectionFn | none | Your own rule. Replaces the type check that connectionMode makes. |
allowCycles | boolean | true | Set to false to reject a wire that closes a loop. |
Write your own connection rule
Pass isValidConnection when the rule depends on more than socket types. The canvas calls it
when you let go over a socket, after its own checks for direction, for two different entities
and for sockets that exist. If it returns false, the canvas rejects the wire.
Your rule replaces connectionMode, so the canvas no longer checks types for you. Call
areTypesCompatible inside the rule if you still want them checked. The socketTypes argument
is your table merged over the built-in socket types.
This rule rejects a second wire into an input that is already connected:
editor.tsximport { areTypesCompatible, KookieFlow, useGraph, type Entity, type IsValidConnectionFn,} from '@kushagradhawan/kookie-flow';export function Editor({ initialEntities }: { initialEntities: Entity[] }) { const { entities, edges, onEntitiesChange, onEdgesChange, onConnect } = useGraph({ initialEntities }); const isValidConnection: IsValidConnectionFn = (params, socketTypes) => { // The drag can start at either end, so find the input first. const input = params.source.isInput ? params.source : params.target; const taken = edges.some((edge) => edge.target === input.entityId && edge.targetSocket === input.socketId); return !taken && areTypesCompatible(params.sourceSocketType, params.targetSocketType, socketTypes); }; return ( <KookieFlow entities={entities} edges={edges} onEntitiesChange={onEntitiesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} isValidConnection={isValidConnection} /> );}IsValidConnectionFn is (params: ConnectionValidationParams, socketTypes: Record<string, SocketType>) => boolean.
ConnectionValidationParams field | Type | Meaning |
|---|---|---|
source | SocketHandle | The socket the drag started from. It can be an input. |
target | SocketHandle | The socket under the pointer. |
sourceSocketType | string | The type of source. |
targetSocketType | string | The type of target. |
Unlike a Connection, these params follow the drag rather than the data flow. When someone
drags from an input to an output, params.source is the input.
A SocketHandle names one socket:
| Field | Type | Meaning |
|---|---|---|
entityId | string | The entity the socket belongs to. |
socketId | string | The socket's id. |
isInput | boolean | true for an input, false for an output. |
Reject cycles
allowCycles defaults to true, so a wire can close a loop. Set it to false when your graph
has to run in order, such as a processing pipeline, where a loop has no first step.
<KookieFlow entities={entities} edges={edges} onConnect={onConnect} allowCycles={false} />With allowCycles={false}, the canvas rejects a wire when its target entity already leads
back to its source entity through existing edges. The wire turns the destructive colour as
soon as it reaches such a socket. The loop check runs after connectionMode or your own
isValidConnection, so it applies with either one. To ask the same question in your own code,
use wouldCreateGraphCycle from Graph queries.
Follow a drag from start to end
onConnectStart runs as soon as the pointer goes down on a socket, before it moves.
onConnectEnd runs when you let go, after onConnect if the wire was accepted.
OnConnectStartParams field | Type | Meaning |
|---|---|---|
entityId | string | The entity the drag started on. |
socketId | string | The socket the drag started on. |
isInput | boolean | Whether that socket is an input. |
ConnectionEndState field | Type | Meaning |
|---|---|---|
isValid | boolean | true when the wire was accepted and onConnect was called. |
source | { entityId: string; socketId: string; isInput: boolean } | The socket the drag started on. |
position | XYPosition | Where the pointer was let go, in world coordinates, the space entity positions use. |
onConnectEnd isn't called when Escape cancels the drag. isValid is false both when the
wire is let go over empty canvas and when it is let go over a socket that rejects it.
A common use is to create a node where a wire is dropped, already connected to the socket the drag started from:
editor.tsximport { KookieFlow, useGraph, type ConnectionEndState, type Entity } from '@kushagradhawan/kookie-flow';export function Editor({ initialEntities }: { initialEntities: Entity[] }) { const { entities, edges, onEntitiesChange, onEdgesChange, onConnect } = useGraph({ initialEntities }); function handleConnectEnd(_event: PointerEvent, { isValid, source, position }: ConnectionEndState) { if (isValid) return; const id = `node-${Date.now()}`; const socket = { id: 'value', name: 'Value', type: 'any' }; // A drag from an input needs a node with an output, placed to the left of the drop point. const fromInput = source.isInput; onEntitiesChange([{ type: 'add', entity: fromInput ? { id, type: 'default', position: { x: position.x - 200, y: position.y }, width: 200, data: { label: 'New node' }, outputs: [socket] } : { id, type: 'default', position, width: 200, data: { label: 'New node' }, inputs: [socket] }, }]); onEdgesChange([{ type: 'add', edge: fromInput ? { id: `${id}-wire`, source: id, sourceSocket: 'value', target: source.entityId, targetSocket: source.socketId } : { id: `${id}-wire`, source: source.entityId, sourceSocket: source.socketId, target: id, targetSocket: 'value' }, }]); } return ( <KookieFlow entities={entities} edges={edges} onEntitiesChange={onEntitiesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} onConnectEnd={handleConnectEnd} /> );}Because isValid is also false for a rejected wire, this handler also adds a node when a
wire is let go over a socket that rejects it.
Check compatibility in your own code
The canvas exports the checks it uses, so a menu, a shortcut or an import step can apply the same rules.
| Function | Returns true when |
|---|---|
areTypesCompatible(typeA, typeB, socketTypes) | The two types are compatible. The order of the types doesn't matter. |
isSocketCompatible(source, target, entitiesOrMap, socketTypes) | The two sockets face opposite ways, belong to different entities, both exist, and have compatible types. This is the check behind the wire's colour during a drag, and behind an edge's invalid. |
validateConnection(source, target, entitiesOrMap, socketTypes, connectionMode?, isValidConnection?) | The canvas would accept the wire when you let go, apart from the loop check. connectionMode defaults to 'loose', and isValidConnection replaces the type check when you pass it. |
source and target are SocketHandle objects. entitiesOrMap takes an Entity[] or a
Map<string, Entity>. Pass a map when you check many sockets, because the function then looks each
entity up directly instead of searching the array.
This checks a wire before a button adds it:
import { validateConnection, type Edge, type Entity, type SocketType } from '@kushagradhawan/kookie-flow';export function connectIfAllowed( entities: Entity[], edges: Edge[], socketTypes: Record<string, SocketType>): Edge[] { const output = { entityId: 'noise', socketId: 'image', isInput: false }; const input = { entityId: 'blur', socketId: 'image', isInput: true }; if (!validateConnection(output, input, entities, socketTypes, 'strict')) return edges; return [...edges, { id: 'noise-to-blur', source: 'noise', sourceSocket: 'image', target: 'blur', targetSocket: 'image' }];}