Timelinx

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:

ContextEngine-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>
  );
}

On this page