npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@videojs/store

v10.0.0-beta.23

Published

Reactive state management for external systems.

Downloads

42,562

Readme

@videojs/store

package-badge

⚠️ Beta Close to stable. Experimental adoption in real projects.

A reactive store for managing state owned by external systems. Built for media players, streaming libraries, and real-time systems where you don't own the state.

npm install @videojs/store

Why?

Traditional state management assumes you own the state. But when working with a <video> element, Web Sockets, streaming libraries, and real-time systems, the external system is the authority. You observe it, send requests to it, and react to its changes.

@videojs/store embraces this model:

  • Read Path: Observe external state, sync to reactive store
  • Write Path: Send requests to the target, handle failures
import { createStore, defineSlice } from '@videojs/store';

const volumeSlice = defineSlice<HTMLMediaElement>()({
  state: () => ({ volume: 1 }),
  attach: ({ target, set, signal }) => {
    const sync = () => set({ volume: target.volume });
    target.addEventListener('volumechange', sync, { signal });
  },
});

const store = createStore<HTMLMediaElement>()(volumeSlice);
store.attach(videoElement);

// State is flat on the store
const { volume } = store;

Core Concepts

Target

The target is a reference to the external system. Slices read from and write to it.

const videoElement = document.querySelector('video');
store.attach(videoElement);

Slices

A slice defines state, how to sync it from the target, and actions to modify the target.

import { defineSlice } from '@videojs/store';
import { listen } from '@videojs/utils/dom';

const volumeSlice = defineSlice<HTMLMediaElement>()({
  state: ({ target }) => ({
    volume: 1,
    muted: false,

    // Sync - use target() directly
    setVolume(value: number) {
      const media = target();
      media.volume = Math.max(0, Math.min(1, value));
    },

    // Action - directly updates target
    toggleMuted() {
      const media = target();
      media.muted = !media.muted;
      return media.muted;
    },
  }),

  attach({ target, signal, set }) {
    const sync = () => set({ volume: target.volume, muted: target.muted });

    sync();

    listen(target, 'volumechange', sync, { signal });
  },
});

Slice Type Inference

State types are fully inferred from the slice config:

import type { InferSliceState } from '@videojs/store';

const volumeSlice = defineSlice<HTMLMediaElement>()({
  state: () => ({ volume: 1, muted: false, /* actions */ }),
  // ...
});

// Infer types from the slice
type VolumeState = InferSliceState<typeof volumeSlice>;
// { volume: number; muted: boolean; setVolume: ...; toggleMuted: ... }

Combining Slices

Use combine to merge multiple slices into one:

import { combine, createStore, defineSlice } from '@videojs/store';

const volumeSlice = defineSlice<HTMLMediaElement>()({ /* ... */ });
const playbackSlice = defineSlice<HTMLMediaElement>()({ /* ... */ });

// Combine into a single slice
const mediaSlice = combine(volumeSlice, playbackSlice);
const store = createStore<HTMLMediaElement>()(mediaSlice);

Behavior:

  • State factories are called in order, results merged (last wins on conflict)
  • All attach handlers run; errors are caught and reported via reportError
  • Use UnionSliceState<Slices> for combined state type inference
import type { UnionSliceState } from '@videojs/store';

const slices = [volumeSlice, playbackSlice] as const;
type MediaState = UnionSliceState<typeof slices>;

Actions

Actions modify the target. You can call target() to access the attached target.

state: ({ target }) => ({
  volume: 1,

  // Action
  setVolume(volume: number) {
    const media = target();
    media.volume = volume;
    return media.volume;
  },

  // Fire-and-forget
  logVolume() {
    console.log('Current volume:', target().volume);
  },
}),

Store

The store connects a slice to a target.

// Simple
const store = createStore<HTMLMediaElement>()(volumeSlice);

// With combined slices and options
const store = createStore<HTMLMediaElement>()(
  combine(volumeSlice, playbackSlice),
  {
    onSetup: ({ store, signal }) => {
      // Called when store is created
    },

    onAttach: ({ store, target, signal }) => {
      // Called when target is attached
    },

    onError: ({ error, store }) => {
      // Global error handler
    },
  }
);

Type Inference

import type { InferStoreState, InferStoreTarget } from '@videojs/store';

const store = createStore<HTMLMediaElement>()(volumeSlice);

type State = InferStoreState<typeof store>;
type Target = InferStoreTarget<typeof store>;

Attaching a Target

const detach = store.attach(videoElement);

// State syncs from target (flat access)
const { paused, volume } = store;

// Actions go to target (flat access)
store.play();
store.setVolume(0.5);

// Detach when done
detach();

Destroying a Store

Clean up when the store is no longer needed:

// Detaches target, aborts signals, cleans up
store.destroy();

Subscribing to State

State is reactive—subscribe to be notified when any property changes:

const unsubscribe = store.subscribe(() => {
  const { volume } = store;
  console.log('State changed:', volume);
});

Mutations are auto-batched—multiple changes in the same tick trigger only one notification.

Cancellation Signals

Use signals to manage cancellation for async operations. The store provides an AbortControllerRegistry instance that tracks the attach lifecycle and supports keyed cancellation for superseding work.

state: ({ target, signals }) => ({
  // Supersede pattern: new seek cancels previous seek
  async seek(time: number) {
    const signal = signals.supersede(signalKeys.seek);
    // ...
  },

  // Cancel all pending operations (e.g., when loading new source)
  loadSource(src: string) {
    signals.clear();
    // ...
  },
}),

API:

| Method | Description | |--------|-------------| | signals.base | Attach-scoped signal. Aborts on detach or reattach. | | signals.supersede(key) | Returns signal that aborts when same key is superseded or base aborts. | | signals.clear() | Aborts all keyed signals, leaving base intact. |

Define shared keys for cross-slice coordination:

export const signalKeys = {
  seek: Symbol.for('@videojs/seek'),
} as const;

Error Handling

Handle errors locally via try/catch, or globally via onError:

import { isStoreError } from '@videojs/store';

// Global error handling
const store = createStore<HTMLMediaElement>()(volumeSlice, {
  onError: ({ error, store }) => {
    console.error('Store error:', error);
  },
});

// Local error handling
try {
  await store.play();
} catch (error) {
  if (isStoreError(error)) {
    switch (error.code) {
      case 'NO_TARGET':
        // No media element attached
        break;
      default:
        console.error(`[${error.code}]`, error.message);
    }
  }
}

All store errors include a code for programmatic handling:

| Code | Description | | ------------ | ---------------------------- | | DESTROYED | Store destroyed | | NO_TARGET | No target attached |

State Primitives

The store uses explicit state containers internally. You can use these primitives directly:

import { createState, flush, isState } from '@videojs/store';

// Create state container
const state = createState({ volume: 1, muted: false });

// Read via .current
const { volume } = state.current; // 1

// Mutate via patch() - changes are auto-batched
state.patch({ volume: 0.5 });
state.patch({ volume: 0.5, muted: true });
// Only ONE notification fires (after microtask)

// Subscribe to changes
state.subscribe(() => {
  const { volume } = state.current;
  console.log('Changed:', volume);
});

// Optional abort signal for cleanup
const controller = new AbortController();
state.subscribe(() => {}, { signal: controller.signal });
controller.abort();

// Check if value is state
isState(state); // true

// Force immediate notification (mainly for tests)
flush();

How It's Different

| | Redux/Zustand | React Query | @videojs/store | | ----------------- | ---------------- | --------------------- | -------------------------- | | Authority | You own state | Server owns state | External system owns state | | Mutations | Sync reducers | Async server requests | Actions to target | | State source | Internal store | HTTP cache | Synced from target | | Subscriptions | To store changes | To query cache | To target events | | Use case | App state | Server data | Media, WebSocket, hardware |

Redux/Zustand: Great for state you control. But when a <video> element is the source of truth, you end up fighting the pattern—syncing external state into the store, handling race conditions between your state and the element's actual state.

React Query: Perfect for server state with request/response. But media elements aren't request/response—they're live, event-driven systems with their own lifecycle.

@videojs/store: Built for external authority. The target is the source of truth. You observe it, request changes, and react to its events.

// Redux approach - fighting the abstraction
dispatch(play());
// Hope the video actually plays...
// Manually sync video.paused back to store...
// Handle race conditions...

// @videojs/store - working with the abstraction
store.play();                 // Actions call into the target
const { paused } = store;     // Always reflects video.paused

Community

If you need help with anything related to Video.js v10, or if you'd like to casually chat with other members:

License

Apache-2.0