A real-time collaborative todo application demonstrating the power of Loro CRDTs with React, using the @loro-extended/change and @loro-extended/repo packages.
Loro is a high-performance CRDT (Conflict-free Replicated Data Type) library that enables automatic synchronization and conflict resolution for collaborative applications. CRDTs allow multiple users to edit the same data simultaneously without conflicts - changes merge automatically and consistently across all peers.
This todo app showcases:
- Real-time collaboration - Multiple users can add, edit, and delete todos simultaneously
- Offline-first - Works offline and syncs when reconnected
- Automatic conflict resolution - No merge conflicts, ever
- React integration - Seamless reactive updates using the
useDocumenthook
The app uses two key packages from this monorepo, via @loro-extended/react:
Provides an Immer-style API for mutating Loro documents. Instead of using Loro's low-level operations, you can write natural JavaScript:
import { change } from "@loro-extended/change";
change(doc, (d) => {
d.todos.push({ id: "...", text: "New todo", completed: false });
});Manages document synchronization across peers with pluggable storage and network adapters:
- Client: Uses IndexedDB for persistence and Server-Sent Events for real-time sync
- Server: Uses LevelDB for persistence and broadcasts changes to all connected clients
pnpm installpnpm -w buildcd examples/todo-app
pnpm devThis starts:
- React app on http://localhost:5173
- Express sync server on http://localhost:5170
Open multiple browser windows to see real-time collaboration in action!
The heart of the integration is the useDocument hook:
import { useDocument, useValue } from "@loro-extended/react";
import { change, Shape } from "@loro-extended/change";
const TodoSchema = Shape.doc({
todos: Shape.list(
Shape.struct({
id: Shape.plain.string(),
text: Shape.plain.string(),
completed: Shape.plain.boolean(),
})
),
});
function TodoApp() {
const doc = useDocument("todo-doc", TodoSchema);
const todos = useValue(doc.todos);
const addTodo = (text: string) => {
doc.todos.push({
id: crypto.randomUUID(),
text,
completed: false,
});
};
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}doc- A typed document reference (stable, auto-syncs)useValue(doc.field)- Subscribe to reactive updates for a specific field
The document state automatically updates when:
- Local changes are made
- Remote changes are received from other peers
- The document is loaded from storage
The Repo handles all the complexity:
- Announces documents to peers
- Requests missing documents
- Merges concurrent changes
- Persists to local storage
Full TypeScript support with document schemas:
interface TodoDoc {
todos: Todo[];
}src/
├── client/ # React application
│ ├── App.tsx # Main component with todo logic
│ ├── hooks/ # Custom React hooks
│ └── contexts/ # React contexts for Repo
├── server/ # Express sync server
│ └── server.ts # SSE endpoint for peer sync
└── shared/ # Shared types