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

react-vibe-state

v0.3.0

Published

Reactive state management with cross-tab sync and persistence powered by Valtio, Yjs, IndexedDB and BroadcastChannel

Readme

🌊 react-vibe-state

Reactive state management with automatic persistence and cross-tab synchronization. Use state almost like a plain JavaScript object - persistence and sync happen transparently in the background.

Features

  • Zero-config persistence - state automatically saved to IndexedDB
  • Cross-tab sync - changes instantly propagate between browser tabs via BroadcastChannel
  • Reactive - powered by Valtio proxy, React components re-render on relevant changes only
  • Type-safe - full TypeScript support with inferred types for state, selectors, and actions
  • Modular - organize state with slices, each with own selectors and actions
  • Conflict-free - CRDT-based sync via Yjs handles concurrent edits gracefully

Under the hood:

  • Valtio - reactive proxy-based state
  • Yjs - CRDT for conflict-free merging
  • valtio-y - two-way binding between Valtio proxy and Yjs doc
  • y-indexeddb - IndexedDB persistence provider
  • y-webrtc - BroadcastChannel sync (WebRTC disabled)

Installation

npm install react-vibe-state

Peer dependency: React 18 or 19

Quick Start

import { createState } from 'react-vibe-state';

// Create state (singleton, typically in a separate file)
const appState = createState({
  name: 'app',
  initial: { count: 0 },
  selectors: {
    isPositive() { return this.count > 0; },
    doubled() { return this.count * 2; },
  },
  actions: {
    increment() { this.count++; },
    decrement() { this.count--; },
  },
});

// Use in React component
function Counter() {
  const { state, selectors, actions } = appState.useSnapshot();
  
  return (
    <div>
      <span>{state.count} (doubled: {selectors.doubled()})</span>
      <button onClick={actions.increment}>+</button>
      <button onClick={actions.decrement} disabled={!selectors.isPositive()}>-</button>
    </div>
  );
}

Open the app in multiple tabs - counter stays in sync automatically.

API Reference

createState(config)

Creates a reactive state instance. Instances are cached by name (safe for HMR).

const state = createState({
  name: 'my-state',
  initial: { /* ... */ },
  selectors: { /* ... */ },
  actions: { /* ... */ },
  slices: { /* key: slice, ... */ },
  // ... options
});

Config Options

| Option | Type | Default | Description | | ------------------------------ | ---------------------------- | -------------- | -------------------------------------------------------- | | persistAndSync | boolean | true | Enable browser storage persistence and cross-tab sync | | storage | "indexed-db" | "indexed-db" | Storage backend | | name | string | required | Unique identifier (letters, numbers, _, -) | | generation | string \| null | null | Version identifier; changing it purges old data | | initial | T \| () => T | required | Initial state object or factory function | | selectors | object | {} | Methods for derived values (this = readonly state) | | actions | object | {} | Methods for mutations (this = mutable state) | | slices | { [key]: Slice } | {} | Object of slices created with createSlice() | | readyTimeout | number | 5000 | Max ms to wait for storage initialization | | validate | (state) => boolean | null | Validation function for state structure | | onReady | () => void | null | Called when initialization completes | | onError | (error) => void | null | Called on initialization failure | | onUpdateValidationFail | (state, origin, sliceKey?) => void | null | Called when storage or remote update fails validation |

Returned State Instance

Properties:

| Property | Type | Description | | ----------- | --------------------------- | ------------------------------------------------------------ | | state | object | Mutable proxy - read/write directly or via selectors/actions | | selectors | object | Bound selector methods (this = readonly state) | | actions | object | Bound action methods (this = mutable state) | | ready | Promise<void> | Resolves when persistence is loaded | | isReady | boolean | Whether initialization completed | | storage | "indexed-db" \| undefined | Used storage type or undefined if disabled |

Methods:

| Method | Description | | ----------------------- | ------------------------------------------------------------ | | useSnapshot() | React hook returning { state, selectors, actions } | | useSnapshot(sliceKey) | React hook scoped to a specific slice { state, selectors, actions } | | reset() | Reset entire state to initial values | | reset(sliceKey) | Reset specific slice to initial values |

createSlice(config)

Creates a modular slice of state with its own selectors and actions.

const todosSlice = createSlice({
  initial: { items: [], filter: 'all' },
  selectors: {
    filtered() {
      return this.filter === 'all' 
        ? this.items 
        : this.items.filter(t => t.status === this.filter);
    },
  },
  actions: {
    add(text: string) {
      this.items.push({ id: Date.now(), text, status: 'active' });
    },
  },
});

// Use in createState
const appState = createState({
  name: 'app',
  initial: {},
  slices: { todos: todosSlice },
});

// Access slice
appState.state.todos.items;
appState.actions.todos.add('Buy milk');
appState.selectors.todos.filtered();

Slice Config Options

| Option | Type | Default | Description | | ------------------------------ | -------------------- | ----------- | ------------------------------------------------ | | initial | T \| () => T | required | Initial slice state object or factory function | | selectors | object | {} | Slice-scoped selectors (this = slice state) | | actions | object | {} | Slice-scoped actions (this = slice state) | | validate | (state) => boolean | undefined | Slice-specific validation | | onUpdateValidationFail | (state, origin) => void | undefined | Called when slice storage or remote validation fails |

useSnapshot(state) / useSnapshot(state, sliceKey)

Standalone hook alternative to state.useSnapshot().

import { useSnapshot } from 'react-vibe-state';

function Component() {
  const { state, selectors, actions } = useSnapshot(appState);
  // or scoped to slice:
  const { state, selectors, actions } = useSnapshot(appState, 'todos');
}

Note on actions: Actions are included in useSnapshot() for convenience, even though semantically they don't operate on the snapshot. Actions always mutate the actual state (not the snapshot), and these mutations trigger reactivity updates across all subscribers.

Validation & Recovery

const appState = createState({
  name: 'app',
  initial: { version: 1, data: [] },
  
  validate: (state) => {
    return typeof state.version === 'number' && Array.isArray(state.data);
  },
  
  onUpdateValidationFail: (invalidData, origin) => {
    if (origin === 'storage') {
      console.warn('Stored data invalid', JSON.stringify(invalidData));
    }
    else {
      // Storage / Remote data incompatible - probably has newer schema
      location.reload();
    }
  },
});

Validation behavior:

  • Initial state - if invalid then throws error (fix your code)
  • Storage/remote updates - if invalid then calls onUpdateValidationFail & throws error (always, when validate is set)

Generation (Schema Versioning)

Useful for session-scoped page state. When your state schema changes incompatibly, change the generation to purge old data:

const appState = createState({
  name: 'app',
  generation: 'v2', // Changed from 'v1' - old data will be deleted
  initial: { /* new schema */ },
});

SSR / Server Components

State works in SSR environments - persistence and sync are automatically disabled server-side. The ready promise resolves immediately.

TypeScript

Full type inference for state, selectors, and actions:

interface User {
  id: string;
  name: string;
  role: 'admin' | 'user';
}

interface UsersState {
  list: User[];
  selectedId: string | null;
}

const usersSlice = createSlice({
  initial: { list: [], selectedId: null } as UsersState,
  selectors: {
    selected() {
      return this.list.find(u => u.id === this.selectedId);
    },         // ^? User | undefined
    admins() {
      return this.list.filter(u => u.role === 'admin');
    },         // ^? User[]
  },
  actions: {
    add(user: User) {
      this.list.push(user);
    },
    select(id: string | null) {
      this.selectedId = id;
    },
  },
});

interface AppState {
  theme: 'dark' | 'light';
}

const appState = createState({
  name: 'app',
  initial: { theme: 'dark' } as AppState,
  slices: { users: usersSlice },
});

// All types are inferred
appState.state.theme;                   // 'dark' | 'light'
appState.state.users.list;              // User[]
appState.selectors.users.selected();    // User | undefined
appState.actions.users.add(user);       // add requires (user: User) => void

Limitations

When persistence is enabled (persistAndSync: true), state is synced via Yjs CRDT which has some constraints:

  • No undefined values - use null instead, or omit/delete the property
  • No functions or class instances - state must be JSON-serializable (primitives, objects, arrays)
  • Truncating arrays - use array.splice(index) or assign a new array instead of array.length = N
// Good
const slice = createSlice({
  initial: {
    selectedId: null,        // use null instead of undefined
    items: [],
    count: 0,
  },
  actions: {
    clearItems() {
      this.items.splice(0);  // use splice to clear
      // or: this.items = [];
    },
  },
});

// Bad - will throw error
const slice = createSlice({
  initial: {
    selectedId: undefined,   // undefined not allowed
    callback: () => {},      // functions not serializable
  },
  actions: {
    clearItems() {
      this.items.length = 0; // don't truncate via length
    },
  },
});

Browser Support

Requires IndexedDB and BroadcastChannel support. Works in all modern browsers. For older browsers without indexedDB.databases() API, an error is thrown - you can conditionally disable persistence:

createState({
  persistAndSync: typeof indexedDB?.databases === 'function',
  // ...
});

Demo App

Note: Demo app is only available in the GitHub repository, not in the npm package.

The test-server/ directory contains a demo application showcasing all library features:

  • Counter - basic counter state
  • Todos - array management with filtering
  • Messages - real-time message sync
  • Users - complex slice with selectors
# Install dependencies
npm install

# Run demo app
npm run dev

Open the app in multiple browser tabs to see cross-tab synchronization in action.

License

ISC