Miraiclip SDK
Build an editor

Reading state

Re-render your UI when the project changes.

project.subscribe(selector, listener) calls listener only when the selected slice changes. Subscribe to the slices a component renders, not the whole state.

main.ts
import { project } from "../shared/project";

const status = document.querySelector("#status")!;

function render() {
  const { playheadUs, selection } = project.getState();
  status.textContent = `${(playheadUs / 1e6).toFixed(2)}s · ${selection.length} selected`;
}

render();
const stops = [
  project.subscribe((s) => s.playheadUs, render),
  project.subscribe((s) => s.selection, render),
];

document.querySelector("#undo")!.addEventListener("click", () => project.undo());

// When the view goes away:
// stops.forEach((stop) => stop());
StatusBar.tsx
import { useCallback, useSyncExternalStore } from "react";
import type { Project, ProjectState } from "@miraiclip/core";

function useProjectValue<T>(project: Project, select: (s: ProjectState) => T): T {
  const subscribe = useCallback(
    (onChange: () => void) => project.subscribe(select, onChange),
    [project, select],
  );
  const read = () => select(project.getState());
  return useSyncExternalStore(subscribe, read, read);
}

// Selectors live outside the component so their identity is stable.
const selectPlayhead = (s: ProjectState) => s.playheadUs;
const selectSelection = (s: ProjectState) => s.selection;

export function StatusBar({ project }: { project: Project }) {
  const playheadUs = useProjectValue(project, selectPlayhead);
  const selection = useProjectValue(project, selectSelection);

  return (
    <div>
      {(playheadUs / 1e6).toFixed(2)}s · {selection.length} selected
      <button onClick={() => project.undo()}>Undo</button>
    </div>
  );
}
StatusBar.vue
<script setup lang="ts">
import { onScopeDispose, shallowRef } from "vue";
import type { Project } from "@miraiclip/core";

const { project } = defineProps<{ project: Project }>();

const playheadUs = shallowRef(project.getState().playheadUs);
const selection = shallowRef(project.getState().selection);

const stops = [
  project.subscribe((s) => s.playheadUs, (v) => (playheadUs.value = v)),
  project.subscribe((s) => s.selection, (v) => (selection.value = v)),
];
onScopeDispose(() => stops.forEach((stop) => stop()));
</script>

<template>
  <div>
    {{ (playheadUs / 1e6).toFixed(2) }}s · {{ selection.length }} selected
    <button @click="project.undo()">Undo</button>
  </div>
</template>
StatusBar.svelte
<script lang="ts">
  import type { Project } from "@miraiclip/core";

  let { project }: { project: Project } = $props();
  let playheadUs = $state(0);
  let selection = $state.raw<string[]>([]);

  $effect(() => {
    ({ playheadUs, selection } = project.getState());
    const stops = [
      project.subscribe((s) => s.playheadUs, (v) => (playheadUs = v)),
      project.subscribe((s) => s.selection, (v) => (selection = v)),
    ];
    return () => stops.forEach((stop) => stop());
  });
</script>

<div>
  {(playheadUs / 1e6).toFixed(2)}s · {selection.length} selected
  <button onclick={() => project.undo()}>Undo</button>
</div>
SlicePathChanges on
Playheads.playheadUsEvery played frame (60×/s during playback)
Selections.selectionproject.setSelection(ids)
Documents.docAny command, undo or redo
Clips, trackss.doc.clips, s.doc.tracks, s.doc.trackOrderClip and track commands

The playhead updates every frame

Keep components that read playheadUs small, or update the DOM directly in the listener without re-rendering.