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

yjs-zustand

v0.1.0

Published

Yjs middleware for Zustand — real-time collaboration via CRDTs

Readme

yjs-zustand

Yjs middleware for Zustand. Sync any Zustand store with a Y.Doc for real-time collaboration.

npm install yjs-zustand yjs

Quick Start

import * as Y from "yjs";
import { create } from "zustand";
import { yjs } from "yjs-zustand";

const useStore = create(
  yjs("shared", (set) => ({
    count: 0,
    increment: () => set((s) => ({ count: s.count + 1 })),
  })),
);

const doc = new Y.Doc();
useStore.yjs.connect(doc);

The store works locally before connect(). Once connected, local changes push to the Y.Doc and remote changes pull into the store. Connect the doc to any Yjs provider for multiplayer.

Usage with a Provider

import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
import { createYjsStore } from "yjs-zustand";

const store = createYjsStore("shared", (set) => ({
  todos: [] as { id: string; text: string; done: boolean }[],
  addTodo: (text: string) =>
    set((s) => ({
      todos: [...s.todos, { id: crypto.randomUUID(), text, done: false }],
    })),
  toggleTodo: (id: string) =>
    set((s) => ({
      todos: s.todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)),
    })),
}));

function joinRoom(roomName: string) {
  const doc = new Y.Doc();
  const provider = new WebsocketProvider("wss://your-server.com", roomName, doc);
  store.yjs.connect(doc);

  return () => {
    store.yjs.disconnect();
    provider.disconnect();
  };
}

API

yjs(mapName, stateCreator, options?)

Zustand middleware. Wraps a state creator and adds a .yjs connection API to the store.

  • mapName — Name for the Y.Map inside the doc (allows multiple stores per doc)
  • stateCreator — Standard Zustand (set, get, api) => state
  • options — See Options

createYjsStore(mapName, stateCreator, options?)

Shorthand for createStore(yjs(...)) with proper typing. Use when you don't need React hooks.

store.yjs

store.yjs.connect(doc);    // Start two-way sync
store.yjs.disconnect();    // Stop syncing
store.yjs.switchRoom(doc); // Disconnect + connect atomically

store.yjs.connected;       // boolean
store.yjs.yMap;            // Y.Map | null
store.yjs.doc;             // Y.Doc | null

Options

yjs("shared", creator, {
  // Keys to exclude from sync (local-only). Supports dot-paths.
  exclude: ["localDraft", "ui.scrollPosition"],

  // Strings stored as plain values instead of Y.Text.
  // Simple names match at any depth; dot-paths match specifically.
  atomicStrings: ["id", "settings.apiKey"],

  // Match array items by key instead of position.
  // Prevents recreating every item when one is inserted/removed.
  arrayKeys: { todos: "id", "projects.tasks": "taskId" },

  // Schema migration
  version: 2,
  migrate: (state, oldVersion) => {
    if (oldVersion < 2) return { ...state, newField: "default" };
    return state;
  },

  // Callbacks
  onSyncStatusChange: (status) => {}, // "syncing" | "synced"
  onConnect: () => {},
  onDisconnect: () => {},
  onError: (error) => {},
});

exclude

Keys that stay local-only and never touch Yjs. Supports dot-paths: "ui.draft" excludes draft inside ui while syncing the rest.

atomicStrings

By default, strings become Y.Text for character-level collaborative editing. For UUIDs, tokens, or hashes, that's wasteful. Mark them atomic to store as plain strings (last-write-wins). Simple names like "id" match at any nesting depth.

arrayKeys

Without this, inserting a todo at index 0 causes every subsequent item to be "changed" in Yjs. With arrayKeys: { todos: "id" }, items are matched by their id field — only the new item is inserted, existing items stay untouched.

version / migrate

When a client with a newer version connects to a doc with an older version, migrate transforms the state. The migrated data is written back to Yjs so other clients don't re-migrate.

onSyncStatusChange

Fires "syncing" when remote changes start being applied and "synced" when done.

onConnect / onDisconnect

Called after connect() and disconnect() complete. Also fires on switchRoom().

onError

Called when an error occurs while applying remote changes. Without this, errors are silently caught to prevent them from bubbling into Yjs internals.

Extensions

Awareness (Presence)

Ephemeral per-client state (cursors, names, status) using the Yjs awareness protocol. Requires y-protocols.

npm install y-protocols
import { createAwareness } from "yjs-zustand";

const presence = createAwareness(provider.awareness, {
  name: "Anonymous",
  cursor: null as { x: number; y: number } | null,
});

presence.setLocal({ name: "Alice", cursor: { x: 10, y: 20 } });
presence.store.getState().peers; // Map<clientId, state>
presence.destroy();

Collaborative Undo/Redo

Uses Yjs's Y.UndoManager to undo only your own changes, not your collaborators'. The store must be connected first.

import { createUndoManager } from "yjs-zustand";

const undo = createUndoManager(store);

undo.undo();
undo.redo();
undo.store.getState().canUndo; // reactive boolean for UI
undo.destroy();

Middleware Composition

Works with other Zustand middleware:

// With immer
create(immer(yjs("shared", (set) => ({ ... }))));

// With devtools
create(devtools(yjs("shared", (set) => ({ ... }))));

// With zundo
create(temporal(yjs("shared", (set) => ({ ... }))));

// All three
create(devtools(immer(yjs("shared", (set) => ({ ... })))));

ORIGIN

The Symbol used as the Yjs transaction origin. Exported so you can distinguish yjs-zustand transactions from your own:

import { ORIGIN } from "yjs-zustand";

doc.on("update", (update, origin) => {
  if (origin === ORIGIN) { /* came from yjs-zustand */ }
});

How It Works

Local:   set() → diff against Y.Map → patch only changed keys → Yjs syncs to peers
Remote:  Yjs observeDeep → convert to plain JS → structural patch → setState
  • Echo prevention — Local transactions are tagged with ORIGIN. The observer skips them.
  • Structural sharing — Unchanged subtrees keep the same JS reference, so React's === checks work.
  • Incremental text diffY.Text is patched with prefix/suffix diffing, not delete-all/reinsert.
  • Functions are never synced — Actions stay local. Remote state merges preserve existing functions.

Type Mapping

| JavaScript | Yjs | Notes | |-----------|-----|-------| | object | Y.Map | Recursive | | array | Y.Array | Recursive, or key-based with arrayKeys | | string | Y.Text | Character-level merging | | string (atomic) | string | Last-write-wins | | number, boolean, null | stored directly | | | Uint8Array | Uint8Array | Binary, native | | function | — | Stripped, never synced |

Not supported: Date (use ISO string), Set/Map (use arrays/objects), BigInt, Symbol, circular references.

License

MIT