Skip to content

Media Player

Problem

Playback commands must update state and call media APIs without mixing side effects into reducers.

Solution

Use reducers for playback data and post-commit effects for the media element.

ts
import { defineMachine } from '@vielzeug/clockwork';

type Event = { type: 'PAUSE' } | { type: 'PLAY' } | { position: number; type: 'SEEK' };
const media = { currentTime: 0, pause: () => console.log('paused'), play: () => console.log('playing') };

const player = defineMachine<{ position: number }, Event>()({
  context: { position: 0 },
  initial: 'paused',
  states: {
    paused: {
      on: {
        PLAY: { effects: [() => media.play()], target: 'playing' },
        SEEK: { reduce: ({ event }) => ({ position: event.position }), target: 'paused' },
      },
    },
    playing: {
      on: {
        PAUSE: { effects: [() => media.pause()], target: 'paused' },
        SEEK: {
          effects: [({ context }) => { media.currentTime = context.position; }],
          reduce: ({ event }) => ({ position: event.position }),
          target: 'playing',
        },
      },
    },
  },
});

const actor = player.createActor();
actor.send({ type: 'PLAY' });
actor.send({ position: 42, type: 'SEEK' });
console.log(actor.snapshot.context.position); // 42
actor.dispose();

Pitfalls

  • Effects receive committed context, never a mutable draft.
  • Effects cannot update context; send an event if later state work is needed.