Write normal JavaScript. Sync in real-time automatically.
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.
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.
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.
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.
See the power of valtio-y in action:
- Simple App - Objects, arrays, and primitives syncing in real-time.
- Sticky Notes - Production-ready collaborative board.
- Whiteboard - Drawing, shapes, and multi-user cursors.
- Todos App - Collaborative list management.
- Minecraft Clone - Multiplayer game state with 3D graphics.
# npm
npm install valtio-y valtio yjs
# pnpm
pnpm add valtio-y valtio yjs
# bun
bun add valtio-y valtio yjsCreate 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.
- β‘ 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)
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.
Core documentation for understanding and using valtio-y effectively:
- Getting Started - Essential patterns for collaboration, initialization, and React integration
- Basic Operations - Objects, arrays, and nested structures
- Core Concepts - Understand CRDTs and the valtio-y mental model
- Structuring Your App - How to organize state with
getRoot - Undo/Redo - Implement undo/redo with Yjs UndoManager
- Performance Guide - Batching, bulk operations, and optimization
Connect a Yjs provider to sync across clients:
import { WebsocketProvider } from "y-websocket";
const provider = new WebsocketProvider("ws://localhost:1234", "room", ydoc);
// State syncs automaticallyWorks with y-websocket, y-partyserver, y-webrtc, y-indexeddb, and more.
β See Getting Started Guide for initialization patterns and provider setup
// 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
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
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
undefinedvalues (usenullor delete the key)- Non-serializable types (functions, symbols, class instances)
- Direct length manipulation (use
array.splice()instead ofarray.length = N)
- 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
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:
- Lexical β Use
@lexical/yjs - TipTap β Use their built-in Yjs extension
- ProseMirror β Use
y-prosemirror
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-integrationbranch 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.
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
undefinedvalues (usenullor delete the property) - Store functions or class instances (not serializable)
- Manipulate
array.lengthdirectly (usesplice()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: