Skip to content

valtiojs/valtio-y

Repository files navigation

valtio-y ⚑

Effortless Collaborative State

Write normal JavaScript. Sync in real-time automatically.

CI npm version bundle size npm downloads TypeScript


Sticky Notes Demo (valtio-y)


Build multiplayer apps as easily as writing local state.

valtio-y synchronizes your Valtio state with Yjs automatically. It solves the difficult edge cases of state-CRDT syncingβ€”like array moves, item replacements, and list reorderingβ€”while remaining significantly more performant than naive bindings.

Examples Β· Guides


Why valtio-y?

Most CRDT bindings struggle with the "last 10%" of complexity: correctly handling array moves (reordering items without deleting/recreating them), replacing objects without breaking references, and maintaining referential equality for React renders.

valtio-y is a ground-up rewrite focused on correctness and performance:

  • Solves Hard Sync Problems: Handles array moves, replacements, and reordering correctly.
  • High Performance: Optimized for bulk operations and deep state trees; significantly faster than other proxy-based solutions.
  • Production Ready: Handles the edge cases that usually cause sync divergence in other libraries.

You get automatic conflict resolution, offline support, and efficient re-renders, but with a level of robustness that stands up to complex real-world usage.

valtio-y handles all of this for you. Just mutate your state like normal:

state.todos.push({ text: "Buy milk", done: false });
state.users[0].name = "Alice";
state.dashboard.widgets[2].position = { x: 100, y: 200 };

// Move item from index 0 to 2 (handled efficiently)
const [todo] = state.todos.splice(0, 1);
state.todos.splice(2, 0, todo);

It automatically syncs across all connected users with zero configuration. No special APIs, no operational transforms to understand, no conflict resolution code to write.

Optimized for Performance

valtio-y batches all mutations in a single event loop tick into one Yjs transaction. This means 100 updates result in just 1 network message.

It also handles large arrays and deep object trees efficiently, only updating the parts of the React component tree that actually changed.

When to Use valtio-y

Perfect for: Real-time collaborative apps involving structured data like Kanban boards, spreadsheets, design tools, game state, and forms.

Not for: Collaborative text editors (Google Docs style). For rich text, use Lexical, TipTap, or ProseMirror with their native Yjs bindings.

Examples

See the power of valtio-y in action:

  1. Simple App - Objects, arrays, and primitives syncing in real-time.
  2. Sticky Notes - Production-ready collaborative board.
  3. Whiteboard - Drawing, shapes, and multi-user cursors.
  4. Todos App - Collaborative list management.
  5. Minecraft Clone - Multiplayer game state with 3D graphics.

Installation

# npm
npm install valtio-y valtio yjs

# pnpm
pnpm add valtio-y valtio yjs

# bun
bun add valtio-y valtio yjs

Quick Start

Create a synchronized proxy and mutate it like any normal object. Changes automatically sync across clients.

import * as Y from "yjs";
import { createYjsProxy } from "valtio-y";

type State = {
  text: string;
  count: number;
  user: { name: string; age: number };
  todos: Array<{ text: string; done: boolean }>;
};

// Create a Yjs document
const ydoc: Y.Doc = new Y.Doc();

// Create a synchronized proxy
// getRoot selects which Yjs structure to sync (all clients must use the same name)
const { proxy: state } = createYjsProxy<State>(ydoc, {
  getRoot: (doc) => doc.getMap("root"), // Most apps use one root Map
});

// Mutate state like a normal object
state.text = "hello";
state.count = 0;

// Nested objects work too
state.user = { name: "Alice", age: 30 };
state.user.age = 31;

// Arrays work naturally
state.todos = [{ text: "Learn valtio-y", done: false }];
state.todos.push({ text: "Build something cool", done: false });
state.todos[0].done = true;

That's it! State is now synchronized via Yjs. Add a provider to sync across clients.


Key Features

  • ⚑ Zero API Overhead - No special methodsβ€”just mutate objects like normal JavaScript
  • 🎯 Fine-Grained Updates - Valtio ensures only components with changed data re-render.
  • 🌐 Offline-First - Local changes automatically merge when reconnected
  • πŸ›‘οΈ Production-Ready - Validation, rollback, comprehensive tests, and benchmarks
  • πŸ”’ Type-Safe - Full TypeScript support with complete type inference
  • πŸ”Œ Provider-Agnostic - Works with any Yjs provider (WebSocket, WebRTC, IndexedDB)

Using with React

Bind your components with Valtio's useSnapshot hook. Components re-render only when their data changes:

import { useSnapshot } from "valtio/react";

function TodoList() {
  const snap = useSnapshot(state);

  return (
    <ul>
      {snap.todos.map((todo, i) => (
        <li key={i}>
          <input
            type="checkbox"
            checked={todo.done}
            onChange={() => (state.todos[i].done = !state.todos[i].done)}
          />
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

Key principle: Read from the snapshot (snap), mutate the proxy (state).

valtio-y works with any framework that Valtio supports: React, Vue, Svelte, Solid, and vanilla JavaScript.

For optimizing large lists with thousands of items, see the Performance Guide.

Note for text inputs: When using controlled text inputs (like <input> or <textarea>), add { sync: true } to prevent cursor jumping:

const snap = useSnapshot(state, { sync: true });
<input value={snap.text} onChange={(e) => (state.text = e.target.value)} />;

This forces synchronous updates instead of Valtio's default async batching. See Valtio issue #270 for details.


Guides

Core documentation for understanding and using valtio-y effectively:


Common Patterns

Setting Up Collaboration

Connect a Yjs provider to sync across clients:

import { WebsocketProvider } from "y-websocket";

const provider = new WebsocketProvider("ws://localhost:1234", "room", ydoc);
// State syncs automatically

Works with y-websocket, y-partyserver, y-webrtc, y-indexeddb, and more.

β†’ See Getting Started Guide for initialization patterns and provider setup

Working with State

// Arrays - all standard methods work
state.items.push(newItem);
state.items[0] = updatedItem;

// Objects - mutate naturally
state.user.name = "Alice";
state.settings = { theme: "dark" };

// Access anywhere (event handlers, timers, async functions)
state.count++;

β†’ See Basic Operations for arrays, objects, and nested structures

Undo/Redo

const {
  proxy: state,
  undo,
  redo,
} = createYjsProxy(ydoc, {
  getRoot: (doc) => doc.getMap("state"),
  undoManager: true, // Enable undo/redo
});

undo(); // Undo last change
redo(); // Redo

β†’ See Undo/Redo Guide for full integration with React and advanced patterns


Performance

valtio-y is fast out of the box with automatic batching, bulk operations, and efficient proxy creation. Typical performance characteristics:

Operation Time Notes
Small updates (1-10 items) ~1-3ms Typical UI interactions
Bulk operations (100 items) ~3-8ms Automatically optimized
Large arrays (1000 items) ~15-30ms Bootstrap/import scenarios
Deep nesting (10+ levels) ~2-4ms Cached proxies stay fast

β†’ See Performance Guide for benchmarking, optimization patterns, and React integration


Limitations

Not Supported

  • undefined values (use null or delete the key)
  • Non-serializable types (functions, symbols, class instances)
  • Direct length manipulation (use array.splice() instead of array.length = N)

What Works

  • Objects and arrays with full support for deep nesting
  • Primitives: string, number, boolean, null
  • All array methods: push, pop, splice, and more
  • Undo/redo via Yjs UndoManager

Research In Progress

Important: valtio-y is designed for shared application state (collaborative data structures like objects, arrays, and primitives), not for building text editors.

If you're building a text editor: Use the native Yjs integration for your editor:

These editors have specialized Yjs integrations optimized for their specific use cases.

Collaborative text integration research:

valtio-y currently focuses on collaborative data structures like maps, arrays, and primitives. Y.Text and Y.Xml* nodes are not part of the supported surface area today because plain strings inside shared objects have covered the real-world use cases we've seen so far. We're still tinkering with richer text and XML nodes on the research/ytext-integration branch, so if you rely on those leaf types we'd love to hear what you're building.

Current status:

  • Core types (Y.Map, Y.Array, primitives) are production-ready with clean, well-tested implementations
  • Notes from the collaborative text/XML prototype remain in the research/ytext-integration branch for anyone curious about the trade-offs we explored

Have a use case for collaborative text in shared state? We'd love to learn more! Please open an issue to discuss your requirements.


Best Practices

Do:

  • Use bootstrap() when initializing state with network sync providers
  • Batch related updates in the same tick for better performance
  • Use bulk array operations (push(...items)) instead of loops
  • Cache references to deeply nested objects in tight loops
  • Store text fields as plain strings

Don't:

  • Use undefined values (use null or delete the property)
  • Store functions or class instances (not serializable)
  • Manipulate array.length directly (use splice() instead)

β†’ See Performance Guide for advanced patterns like concurrent list reordering with fractional indexing


Feedback and contributions welcome! If you find bugs or have suggestions, please open an issue.

For detailed technical documentation, see:

About

Two-way sync between Valtio proxies and Yjs CRDTs

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors