React API
Components, props, refs, and state patterns for @snap-engine/snapsort-react.
The React package wraps SnapSort’s core objects in React components. Use this page when React owns the rendered arrays and SnapSort should provide drag, drop, ordering, and animation behavior.
Install
npm install @snap-engine/core @snap-engine/snapsort @snap-engine/snapsort-react react react-domreact and react-dom are peer dependencies of @snap-engine/snapsort-react.
Import
import {
Engine,
Container,
Item,
Handle,
useSnapSortAwaitMutation,
} from "@snap-engine/snapsort-react";Container and Item are the only components — every container picks its drag behavior with a mode config field.
Import core types when you need typed callbacks or refs:
import type { Engine as CoreEngine } from "@snap-engine/core";
import type {
CanDropEvent,
Container as SnapSortContainer,
ContainerConfig,
DragEndEvent,
DragStartEvent,
DropTargetChangeEvent,
Item as SnapSortItem,
ItemInsertEvent,
ItemMoveEvent,
ItemSnapshotMetadata,
} from "@snap-engine/snapsort";The core Container/Item classes are aliased here (SnapSortContainer/SnapSortItem) to avoid colliding with the React component names of the same name.
Engine
Engine creates or uses a core Engine, assigns it to the rendered DOM root, installs a collision engine, and provides it through React context.
<Engine id="tasks" style={{ minHeight: 320 }}>
<Container config={{ direction: "column", groupID: "tasks" }}>
<Item metadata={{ itemId: "task-1" }}>Task 1</Item>
</Container>
</Engine>Engine Props
type EngineProps = {
children: ReactNode;
className?: string;
debug?: boolean;
engine?: CoreEngine | null;
id?: string;
style?: CSSProperties;
};| Prop | Default | Description |
|---|---|---|
children | Required | SnapSort containers and app UI. |
className | undefined | Extra class string applied to the engine root. |
debug | false | Enables the SnapEngine debug renderer while true. |
engine | null | Optional existing @snap-engine/core engine instance. |
id | "snap-canvas" | ID applied to the engine root element. |
style | undefined | Extra inline styles merged onto the engine root. |
Use useSnapSortEngine() inside descendants when you need the current core engine.
Modes
Every container is created from the same Container component; pick its drag behavior with config.mode. mode defaults to "euclidean".
| Mode | Best for |
|---|---|
"euclidean" (default) | Lists, boards, and general reordering. |
"progressive" | Sentence builders, token banks, and wrapped variable-width rows. |
"insertion" | File trees, outlines, and other drop-marker UIs where the placeholder is a line instead of a full item. |
Item never needs a mode — the same Item works under a container of any mode.
<Container config={{ direction: "column", groupID: "tasks" }}>
<Item metadata={{ itemId: "task-1" }}>Task 1</Item>
</Container>
<Container config={{ mode: "progressive", direction: "row", groupID: "sentence" }}>
<Item metadata={{ itemId: "word-1" }}>hello</Item>
</Container>
<Container config={{ mode: "insertion", direction: "column", groupID: "outline" }}>
<Item metadata={{ itemId: "section-1" }}>Section 1</Item>
</Container>Mode is a property of the whole drag tree, resolved from the root container when a drag starts — nest containers of the same mode.
State Backed Lists
When React owns the item arrays, update state from callbacks.onItemMove. The React adapter wraps mutation callbacks in flushSync and supplies an awaitMutation callback by default, so SnapSort can read committed DOM after React updates.
onItemMove is preferred over the lower-level onItemInsert whenever a drop is really “move this item from one position to another” — it hands you both the source and destination as one event instead of making you reconstruct the move from an insert alone.
import { useCallback, useState } from "react";
import { Engine, Container, Item } from "@snap-engine/snapsort-react";
import type { ItemMoveEvent } from "@snap-engine/snapsort";
type Task = { id: string; label: string };
type Column = { id: string; label: string; tasks: Task[] };
const initialColumns: Column[] = [
{
id: "todo",
label: "To Do",
tasks: [
{ id: "task-1", label: "Sketch layout" },
{ id: "task-2", label: "Wire behavior" },
],
},
{
id: "done",
label: "Done",
tasks: [{ id: "task-3", label: "Create branch" }],
},
];
export function TaskBoard() {
const [columns, setColumns] = useState(initialColumns);
const moveTask = useCallback((taskId: string, columnId: string, index: number) => {
setColumns((currentColumns) => {
let movedTask: Task | null = null;
const withoutTask = currentColumns.map((column) => {
const sourceIndex = column.tasks.findIndex((task) => task.id === taskId);
if (sourceIndex === -1) return column;
const nextTasks = column.tasks.slice();
[movedTask] = nextTasks.splice(sourceIndex, 1);
return { ...column, tasks: nextTasks };
});
if (!movedTask) return currentColumns;
return withoutTask.map((column) => {
if (column.id !== columnId) return column;
const nextTasks = column.tasks.slice();
nextTasks.splice(Math.max(0, Math.min(index, nextTasks.length)), 0, movedTask);
return { ...column, tasks: nextTasks };
});
});
}, []);
const handleItemMove = useCallback(
(event: ItemMoveEvent) => {
const itemId = event.itemMetadata.itemId;
const columnId = event.to.containerMetadata.columnId;
if (typeof itemId !== "string" || typeof columnId !== "string") return;
moveTask(itemId, columnId, event.to.index);
},
[moveTask],
);
return (
<Engine id="task-board">
<Container config={{ direction: "row", name: "board-root", noDrop: true }}>
{columns.map((column) => (
<Container
key={column.id}
metadata={{ columnId: column.id }}
config={{
direction: "column",
groupID: "tasks",
callbacks: {
onItemMove: handleItemMove,
},
}}
>
<h2>{column.label}</h2>
{column.tasks.map((task) => (
<Item key={task.id} metadata={{ itemId: task.id }}>
<article>{task.label}</article>
</Item>
))}
</Container>
))}
</Container>
</Engine>
);
}Use callbacks.onItemRemove when container.removeItem(id) or your own controls should also remove data from React state. Drag moves and moveItem() are driven by onItemMove when it’s defined, and fall back to onItemInsert on the destination container otherwise.
Mutation Timing
The React adapter handles the framework boundary for component callbacks:
onItemInsert,onItemRemove,onGhostInsert, andonGhostRemoveare wrapped influshSync.callbacks.awaitMutationdefaults touseSnapSortAwaitMutation().- State updates in SnapSort callbacks should be normal synchronous
setStatecalls.
Do not put SnapSort-driven state updates behind startTransition, timers, animation frames, or async promise chains. SnapSort needs the DOM committed before it continues from framework mutation into layout reads.
useSnapSortAwaitMutation() is exported for advanced cases where you wire core objects directly or want to pass an explicit awaitMutation callback:
const awaitMutation = useSnapSortAwaitMutation();
<Container
config={{
direction: "column",
groupID: "tasks",
callbacks: {
onItemMove: handleItemMove,
awaitMutation,
},
}}
/>Imperative Moves
Container components forward refs to their core container object. Call moveItem(itemId, targetContainer, index) with the same ID used in metadata.itemId.
import { useRef } from "react";
import { Engine, Container, Item } from "@snap-engine/snapsort-react";
import type { Container as SnapSortContainer } from "@snap-engine/snapsort";
export function ClickMove() {
const source = useRef<SnapSortContainer>(null);
const target = useRef<SnapSortContainer>(null);
function moveToTarget(id: string) {
if (!source.current || !target.current) return;
source.current.moveItem(id, target.current, target.current.numberOfItems);
}
return (
<Engine id="click-move">
<Container ref={source} config={{ direction: "column", groupID: "cards" }}>
<Item metadata={{ itemId: "card-1" }}>
<button type="button" onClick={() => moveToTarget("card-1")}>
Move card
</button>
</Item>
</Container>
<Container ref={target} config={{ direction: "column", groupID: "cards" }} />
</Engine>
);
}When your list is backed by React state, keep the same onItemMove (or onItemInsert) callback on the relevant containers so imperative moves and drag moves update the rendered arrays the same way.
Container
Props
type ContainerProps = {
children?: ReactNode;
className?: string;
config: ContainerConfig;
containerObject?: SnapSortContainer | null;
locked?: boolean;
metadata?: Record<string, unknown>;
style?: CSSProperties;
};| Prop | Default | Description |
|---|---|---|
children | undefined | Item and nested container content. |
className | "" | Extra class string applied to the rendered element. |
config | Required | SnapSort container configuration. |
containerObject | null | Optional existing core container object. When provided, the component does not own creation. |
locked | true | Keeps the container itself from being dragged when nested. Set to false for draggable nested containers. |
metadata | {} | Metadata assigned to the container object and exposed in callback events. |
style | undefined | Extra inline styles merged onto the rendered container element. |
Container renders a div with snapsort-container plus a mode-specific class: snapsort-mode-euclidean, snapsort-mode-progressive, or snapsort-mode-insertion.
Config
| Option | Default | Description |
|---|---|---|
mode | "euclidean" | Drag/drop mode for this tree: "euclidean", "progressive", or "insertion". |
strategy | Resolved from mode | Advanced: a custom { dropTarget, lifecycle } strategy pair, overriding mode. |
direction | "column" | Main layout direction: "column" or "row". |
groupID | "default-group" | Containers with the same group can exchange items. |
mainAxisAlign | "start" | Main-axis alignment for virtual placement. Supports "start" and "center". |
name | Generated | Human-readable container name. |
animation | Default 100ms animations | Reorder, drop, and click-move animation settings. Set to null to disable configured animations. |
disableFlip | false | Disables FLIP movement animation. |
noDrop | false | Prevents the container from being a drop target. Useful for root layout containers. |
dropArea | false | Treats the container as an explicit drop area for collision filtering. |
callbacks | Default DOM callbacks | Hooks for DOM, framework state, and custom ghost behavior. |
Callbacks
| Callback | When it runs |
|---|---|
onItemMove(event) | SnapSort moves an item to a new container/index. Preferred over onItemInsert for state-backed lists — carries both from and to locations. Falls back to onItemInsert on the destination container when not defined. |
onItemInsert(event) | SnapSort inserts an item into a container (the primitive onItemMove falls back to). |
onItemRemove(event) | SnapSort removes an item through removeItem(id). |
onDragStart(event) | A drag is starting, before any ghost/state changes. Return false to veto the drag. |
onDragEnd(event) | A drag has ended and its mutation has been committed. event.destination is null when the item returned to its source or the drag was cancelled. |
onDropTargetChange(event) | The prospective drop container/index changed during a drag. Fires on the root container. |
canDrop(event) | Consulted while resolving candidates for this container; return false to reject drops into it for the current drag. Evaluated once per container per resolution, not once per candidate slot — keep it cheap. |
onGhostInsert(event) | SnapSort inserts or moves a ghost item or insertion marker. |
onGhostRemove(event) | SnapSort removes a ghost item or insertion marker. |
createGhost(event) | SnapSort needs a ghost/marker element. event.kind is "flow" (euclidean/progressive) or "marker" (insertion). Return an element, null, or void. |
awaitMutation() | SnapSort waits for framework-owned DOM updates before reading final layout. Defaults to useSnapSortAwaitMutation(). |
onItemMove, onItemInsert, onItemRemove, onGhostInsert, and onGhostRemove are wrapped in flushSync by the React adapter (see Mutation Timing above). onDragStart and canDrop are called directly (unwrapped) since their return value is needed synchronously and they can run far more often per drag.
Item
Props
type ItemProps = {
children: ReactNode;
className?: string;
itemObject?: SnapSortItem | null;
metadata?: ItemSnapshotMetadata;
style?: CSSProperties;
};| Prop | Default | Description |
|---|---|---|
children | Required | Rendered item content. |
className | "" | Extra class string applied to the rendered element. |
itemObject | null | Optional existing core item object. When provided, the component does not destroy it on unmount. |
metadata | {} | Metadata assigned to the item object. Use metadata.itemId for stable keyed state. |
style | undefined | Extra inline styles merged onto the rendered item element. |
Item forwards a ref to its core item object and renders a div with snapsort-item — the same component and class work under any container mode.
Handle
Handle registers a child element as an input alias for the nearest item.
<Item metadata={{ itemId: "task-1" }}>
<article>
<Handle className="drag-handle">Drag</Handle>
<span>Task 1</span>
</article>
</Item>Props
type HandleProps = {
children: ReactNode;
className?: string;
style?: CSSProperties;
};Handle forwards a ref to the rendered HTMLDivElement and renders a div with snapsort-handle plus the supplied className.
Subpath Exports
The package exports the full API from @snap-engine/snapsort-react, plus these subpaths:
| Import path | Exports |
|---|---|
@snap-engine/snapsort-react/Engine | Engine, SnapSortEngine, EngineContext, useSnapSortEngine |
@snap-engine/snapsort-react/Container | Container components and ContainerObjectContext |
@snap-engine/snapsort-react/Item | Item components and ItemObjectContext |
@snap-engine/snapsort-react/Handle | Handle |
@snap-engine/snapsort-react/useSnapSortAwaitMutation | useSnapSortAwaitMutation |