Hooks
Every @timelinx/react hook - what it returns, when it re-renders, and how to use it
All hooks in @timelinx/react read from the EngineSnapshot via useSyncExternalStore. They subscribe selectively - each hook re-renders only when the slice of state it reads changes.
The rule: use the narrowest hook for what you need. useClip(id) re-renders only when that clip changes. useTimeline() re-renders any time any part of the timeline changes.
State Hooks
useEngine()
Returns the stable TimelineEngine reference. Does not cause re-renders.
const engine = useEngine();
engine.dispatch(transaction);
engine.undo();
engine.activateTool('razor');useTimeline()
Returns the full Timeline object. Re-renders when any timeline metadata changes (name, fps, duration, version).
const timeline = useTimeline();
// { id, name, fps, duration, version, tracks, markers, ... }
return <h1>{timeline.name}</h1>;useTrackIds()
Returns a stable readonly string[] of track IDs in order. Re-renders only when the count or order of tracks changes. Use this to mount/unmount track row components:
const trackIds = useTrackIds();
return (
<>
{trackIds.map((id) => (
<TimelineTrack key={id} trackId={id} />
))}
</>
);useTrack(id)
Returns a single Track | null by ID. Re-renders when that track's data changes (clips, mute, lock, height, name). Does not re-render when other tracks change.
const track = useTrack('v1');
if (!track) return null;
return <span>{track.name} - {track.clips.length} clips</span>;useClip(id)
Returns a single Clip | null by ID. Re-renders when that clip changes (timeline bounds, media bounds, speed, effects, name). Does not re-render when anything else changes.
const clip = useClip('clip-1');
if (!clip) return null;
return <span style={{ color: clip.color ?? 'white' }}>{clip.name}</span>;useMarkers()
Returns readonly Marker[] from timeline.markers. Re-renders when the marker list changes.
const markers = useMarkers();
return <ul>{markers.map(m => <li key={m.id}>{m.label}</li>)}</ul>;useProvisional()
Returns ProvisionalState | null. null when idle; a provisional state object during drag gestures. Re-renders on every pointer move during a drag.
const provisional = useProvisional();
if (provisional) {
// Render ghost clips
return provisional.clips.map((p) => <GhostClip key={p.clipId} {...p} />);
}useSelectedClipIds()
Returns ReadonlySet<string>. Re-renders when the selection set changes.
const selectedClipIds = useSelectedClipIds();
const isSelected = selectedClipIds.has(clip.id);useSelectedCaptionIds()
Returns ReadonlySet<string>. Re-renders when the caption selection set changes.
useActiveTool() / useActiveToolId()
const { id, cursor } = useActiveTool();
// id: 'selection' | 'razor' | ...
// cursor: 'default' | 'crosshair' | null | ...
const toolId = useActiveToolId();Use these to highlight the active tool button in a toolbar.
History Hooks
useCanUndoRedo()
const { canUndo, canRedo } = useCanUndoRedo();
<button disabled={!canUndo} onClick={() => engine.undo()}>Undo</button>
<button disabled={!canRedo} onClick={() => engine.redo()}>Redo</button>Re-renders only when undo/redo availability changes - not on every dispatch.
useCanUndo() / useCanRedo()
Boolean variants of useCanUndoRedo():
const canUndo = useCanUndo();
const canRedo = useCanRedo();useHistory(engine)
Engine-first. Returns detailed history state:
import { useHistory } from '@timelinx/react';
const { canUndo, canRedo, undoLabel, redoLabel } = useHistory(engine);
// undoLabel: 'Undo: Move clip'
// redoLabel: 'Redo: Add track'Playback Hooks
usePlayheadFrame(engine?)
Returns the current TimelineFrame of the playhead. Re-renders on every playhead tick during playback.
import { usePlayheadFrame } from '@timelinx/react';
function Transport({ engine }) {
const frame = usePlayheadFrame(engine);
const tc = framesToTimecode(frame, timeline.fps);
return <span className="timecode">{tc}</span>;
}Mount sparingly
usePlayheadFrame re-renders on every animation frame during playback (30–60× per second). Keep it only in components that display the playhead position - ruler, transport, and compositor. Do not use it in clip components.
useIsPlaying(engine?)
Returns boolean. Re-renders when playback starts or stops.
const isPlaying = useIsPlaying(engine);
<button>{isPlaying ? '⏸ Pause' : '▶ Play'}</button>Clip & Effect Hooks
useClipEffects(engine, clipId)
Returns readonly Effect[] for a clip. Re-renders when the effects array changes.
const effects = useClipEffects(engine, 'clip-1');
effects.forEach((effect) => console.log(effect.type));useClipTransition(engine, clipId)
Returns Transition | null. Re-renders when the clip's transition changes.
const transition = useClipTransition(engine, 'clip-1');useTrackCaptions(engine, trackId)
Returns readonly Caption[]. Re-renders when the caption list on that track changes.
Virtual Rendering Hooks
useVirtualWindow(engine, options)
Returns a VirtualWindow - the visible frame range from current scroll and zoom. See Virtual Rendering.
useVisibleClips(engine, window)
Returns clips that intersect the visible frame window. See Virtual Rendering.
Change Hooks
useChange(engine)
Returns a StateChange diff from the previous snapshot. Use for coarse re-renders - react to the type of change without inspecting the full state:
import { useChange } from '@timelinx/react';
const change = useChange(engine);
// { type: 'CLIP_MOVED' | 'TRACK_ADDED' | ... , clipId?, trackId? }Engine-First Variants
Every context-based hook has a WithEngine variant that accepts the engine explicitly. Use them when building components outside the default TimelineProvider:
| Context | Engine-first |
|---|---|
useTimeline() | useTimelineWithEngine(engine) |
useTrackIds() | useTrackIdsWithEngine(engine) |
useTrack(id) | useTrackWithEngine(engine, id) |
useClip(id) | useClipWithEngine(engine, id) |
useSelectedClipIds() | useSelectedClipIds(engine) |
usePlayheadFrame() | usePlayheadFrame(engine) |
useIsPlaying() | useIsPlaying(engine) |
Dispatching from Components
Hooks read state; transactions still go through the engine:
function DeleteButton({ clipId }: { clipId: string }) {
const engine = useEngine();
return (
<button
onClick={() => {
const result = engine.dispatch({
id: `delete-${clipId}`,
label: 'Delete clip',
timestamp: Date.now(),
operations: [{ type: 'DELETE_CLIP', clipId }],
});
if (!result.accepted) {
console.warn('Delete rejected:', result.reason);
}
}}
>
Delete
</button>
);
}