Skip to main content
EditorCore is the central singleton that manages all editor state through specialized managers. It provides a consistent API for accessing and manipulating the editor’s functionality.

Architecture overview

The EditorCore singleton coordinates all editor operations through domain-specific managers:

Core implementation

The EditorCore class is implemented as a singleton pattern with private constructor:
apps/web/src/core/index.ts
export class EditorCore {
  private static instance: EditorCore | null = null;

  public readonly command: CommandManager;
  public readonly playback: PlaybackManager;
  public readonly timeline: TimelineManager;
  public readonly scenes: ScenesManager;
  public readonly project: ProjectManager;
  public readonly media: MediaManager;
  public readonly renderer: RendererManager;
  public readonly save: SaveManager;
  public readonly audio: AudioManager;
  public readonly selection: SelectionManager;

  private constructor() {
    this.command = new CommandManager();
    this.playback = new PlaybackManager(this);
    this.timeline = new TimelineManager(this);
    this.scenes = new ScenesManager(this);
    this.project = new ProjectManager(this);
    this.media = new MediaManager(this);
    this.renderer = new RendererManager(this);
    this.save = new SaveManager(this);
    this.audio = new AudioManager(this);
    this.selection = new SelectionManager(this);
    this.save.start();
  }

  static getInstance(): EditorCore {
    if (!EditorCore.instance) {
      EditorCore.instance = new EditorCore();
    }
    return EditorCore.instance;
  }

  static reset(): void {
    EditorCore.instance = null;
  }
}

Manager responsibilities

Each manager handles a specific domain of editor functionality:
Controls video playback state, seeking, and frame navigation.
Manages tracks, elements, and timeline operations like splitting, trimming, and moving elements.
Handles scene creation, switching, deletion, and bookmark management.
Manages project-level state including settings, metadata, and active project.
Handles media asset loading, processing, and management.
Controls video rendering and canvas output.
Implements undo/redo functionality through the command pattern.
Handles auto-saving and project persistence.
Manages audio playback, mixing, and processing.
Tracks selected elements and provides selection utilities.

When to use what

The method you use to access EditorCore depends on your context:
Always use the useEditor() hook:
import { useEditor } from '@/hooks/use-editor';

function MyComponent() {
  const editor = useEditor();
  const tracks = editor.timeline.getTracks();

  // Call methods
  editor.timeline.addTrack({ type: 'video' });

  // Display data (auto re-renders on changes)
  return <div>{tracks.length} tracks</div>;
}
The hook:
  • Returns the singleton instance
  • Subscribes to all manager changes
  • Automatically re-renders when state changes
Never instantiate EditorCore directly. Always use getInstance() or the useEditor() hook to ensure you’re working with the singleton instance.

The useEditor hook

The useEditor() hook is implemented using React’s useSyncExternalStore to provide reactive state updates:
apps/web/src/hooks/use-editor.ts
export function useEditor(): EditorCore {
  const editor = useMemo(() => EditorCore.getInstance(), []);
  const versionRef = useRef(0);

  const subscribe = useCallback(
    (onStoreChange: () => void) => {
      const handleStoreChange = () => {
        versionRef.current += 1;
        onStoreChange();
      };

      const unsubscribers = [
        editor.playback.subscribe(handleStoreChange),
        editor.timeline.subscribe(handleStoreChange),
        editor.scenes.subscribe(handleStoreChange),
        editor.project.subscribe(handleStoreChange),
        editor.media.subscribe(handleStoreChange),
        editor.renderer.subscribe(handleStoreChange),
        editor.selection.subscribe(handleStoreChange),
      ];

      return () => {
        for (const unsubscribe of unsubscribers) {
          unsubscribe();
        }
      };
    },
    [editor],
  );

  const getSnapshot = useCallback(() => versionRef.current, []);

  useSyncExternalStore(subscribe, getSnapshot, getSnapshot);

  return editor;
}
The hook subscribes to all manager changes and triggers re-renders when any manager state updates.

Manager subscription pattern

Each manager implements a subscription pattern for reactive updates:
class ExampleManager {
  private listeners = new Set<() => void>();

  subscribe(listener: () => void): () => void {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }

  private notify(): void {
    this.listeners.forEach((fn) => fn());
  }

  // Call notify() after state changes
  updateState(newState) {
    // ... update logic
    this.notify();
  }
}

Best practices

Use the right access method

Use useEditor() in React components and getInstance() in utilities.

Access managers directly

Call manager methods directly: editor.timeline.addTrack() instead of going through EditorCore.

Don't create new instances

Never call new EditorCore(). Always use the singleton pattern.

Let managers handle state

Don’t duplicate manager state in local component state.
  • Timeline system - Understanding the TimelineManager
  • Scenes - Working with the ScenesManager
  • Commands - Using the CommandManager for undo/redo
  • Actions - Triggering operations through the actions system

Build docs developers (and LLMs) love