valtio-y
v1.1.2
Published
Collaborative Valtio state
Downloads
260
Readme
valtio-y
Two-way sync between Valtio proxies and Yjs CRDTs. Build collaborative apps with automatic conflict resolution and offline support—just mutate objects naturally.
state.todos.push({ text: "Buy milk", done: false });
state.users[0].name = "Alice";
// Automatically syncs across all connected usersLive Examples
Open any demo in multiple browser tabs and watch them sync in real-time:
🎮 Minecraft Clone - Simple showcase inspired by Minecraft. Lets multiple users place and remove blocks in real time using Three.js and valtio-y.
🎨 Whiteboard - Collaborative drawing with shapes, colors, and real-time cursors. Google Docs for drawing.
📝 Sticky Notes - Production-ready app running on Cloudflare Workers (this is real infrastructure, not a demo server).
✅ Todos App - Classic collaborative todo list. Real-time updates, no refresh needed.
🧪 Simple Demo – Best for understanding the basic sync patterns (objects, arrays, primitives); other demos above are more production-focused.
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";
// Create a Yjs document
const ydoc = new Y.Doc();
// Create a synchronized proxy
const { proxy: state } = createYjsProxy(ydoc, {
getRoot: (doc) => doc.getMap("root"),
});
// 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.
Installation
# npm
npm install valtio-y valtio yjs
# pnpm
pnpm add valtio-y valtio yjs
# bun
bun add valtio-y valtio yjsReact Integration
Use Valtio's useSnapshot hook to automatically re-render components when 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.
Collaboration Setup
Connect any Yjs provider to sync across clients:
import { WebsocketProvider } from "y-websocket";
const provider = new WebsocketProvider(
"ws://localhost:1234",
"room-name",
ydoc
);
// That's it—state syncs automaticallyWorks with any provider: y-websocket, y-partyserver (great for Cloudflare), y-webrtc, y-indexeddb, etc.
Common Operations
Initializing State
When using network providers, initialize after first sync:
const { proxy: state, bootstrap } = createYjsProxy(ydoc, {
getRoot: (doc) => doc.getMap("state"),
});
provider.once("synced", () => {
bootstrap({
todos: [],
settings: { theme: "light" },
});
// Only writes if the document is empty
});Arrays
state.items.push(newItem);
state.items[0] = updatedItem;
state.items.splice(1, 2, replacement1, replacement2);
const [item] = state.items.splice(2, 1);
state.items.splice(0, 0, item); // Move itemObjects
state.user.name = "Alice";
delete state.user.temporaryFlag;
state.data.deeply.nested.value = 42;Undo/Redo
const {
proxy: state,
undo,
redo,
} = createYjsProxy(ydoc, {
getRoot: (doc) => doc.getMap("state"),
undoManager: true, // Enable with defaults
});
state.count = 1;
undo(); // state.count -> undefined
redo(); // state.count -> 1See API documentation for configuration options.
Features
- Zero API overhead - Just mutate objects like normal JavaScript
- Fine-grained updates - Components re-render only when their data changes
- Offline-first - Changes merge automatically when reconnected
- TypeScript - Full type safety and inference
- Production-ready - Comprehensive tests and benchmarks
- Framework-agnostic - Works with React, Vue, Svelte, Solid, and vanilla JS
Why valtio-y?
Stop writing sync logic. Just mutate objects.
- Valtio gives you reactive state with zero boilerplate
- Yjs gives you conflict-free sync and offline support
- valtio-y connects them - you get both, write neither
No reducers, no actions, no manual sync code. Just: state.count++
Limitations
- Don't use
undefined(usenullor delete the property) - Don't store functions or class instances (not serializable)
- Use
array.splice()instead ofarray.length = N
For text editors, use native Yjs integrations: Lexical, TipTap, or ProseMirror.
API Reference
createYjsProxy(doc, options)
const { proxy, bootstrap } = createYjsProxy(ydoc, {
getRoot: (doc: Y.Doc) => Y.Map<any>,
});Returns:
proxy- Valtio proxy for state mutationsbootstrap(data)- Initialize state (no-op if doc not empty)
