React integration for Loro Mirror - a state management library with Loro CRDT synchronization.
npm install loro-mirror-react loro-mirror loro-crdt
# or
yarn add loro-mirror-react loro-mirror loro-crdt
# or
pnpm add loro-mirror-react loro-mirror loro-crdtimport React, { useMemo } from "react";
import { LoroDoc } from "loro-crdt";
import { schema } from "loro-mirror";
import { useLoroStore } from "loro-mirror-react";
// Define your schema
const todoSchema = schema({
todos: schema.LoroList(
schema.LoroMap({
text: schema.String({ required: true }),
completed: schema.Boolean({ defaultValue: false }),
}),
// Use `$cid` (reuses Loro container id; explained below)
(item) => item.$cid,
),
filter: schema.String({ defaultValue: "all" }),
});
function TodoApp() {
// Create a Loro document
const doc = useMemo(() => new LoroDoc(), []);
// Create a store
const { state, setState } = useLoroStore({
doc,
schema: todoSchema,
initialState: { todos: [], filter: "all" },
});
// Add a new todo (synchronous; the update is applied before return)
const addTodo = (text: string) => {
setState((s) => ({
...s,
todos: [...s.todos, { text, completed: false }],
}));
};
// Rest of your component...
}import React, { useMemo } from "react";
import { LoroDoc } from "loro-crdt";
import { schema } from "loro-mirror";
import { createLoroContext } from "loro-mirror-react";
// Define your schema
const todoSchema = schema({
todos: schema.LoroList(
schema.LoroMap({
text: schema.String({ required: true }),
completed: schema.Boolean({ defaultValue: false }),
}),
(t) => t.$cid, // stable id from Loro container id
),
});
// Create a context
const {
LoroProvider,
useLoroContext,
useLoroState,
useLoroSelector,
useLoroAction,
} = createLoroContext(todoSchema);
// Root component
function App() {
const doc = useMemo(() => new LoroDoc(), []);
return (
<LoroProvider doc={doc} initialState={{ todos: [] }}>
<TodoList />
<AddTodoForm />
</LoroProvider>
);
}
// Todo list component
function TodoList() {
// Subscribe only to the todos array
const todos = useLoroSelector((state) => state.todos);
return (
<ul>
{todos.map((todo) => (
<TodoItem
key={todo.$cid /* stable key from Loro container id */}
todo={todo}
/>
))}
</ul>
);
}
// Todo item component
function TodoItem({ todo }) {
const toggleTodo = useLoroAction((state) => {
const todoIndex = state.todos.findIndex((t) => t.$cid === todo.$cid); // compare by `$cid`
if (todoIndex !== -1) {
state.todos[todoIndex].completed =
!state.todos[todoIndex].completed;
}
});
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={toggleTodo}
/>
<span>{todo.text}</span>
</li>
);
}Creates and manages a Loro Mirror store.
const { state, setState, finalizeEphemeralPatches, store } = useLoroStore({
doc,
schema,
initialState,
validateUpdates,
debug,
});Notes on updates:
setStatefromuseLoroStoreand the setter fromuseLoroStaterun synchronously; subsequent code can read the updated state immediately.useLoroCallbackanduseLoroActionreturn synchronous functions that callsetStateunder the hood.
Subscribes to a specific value from a Loro Mirror store.
const todos = useLoroValue(store, (state) => state.todos);Creates a callback that updates a Loro Mirror store.
const addTodo = useLoroCallback(
store,
(state, text) => {
state.todos.push({ text, completed: false }); // `$cid` is injected from Loro container id
},
[
/* dependencies */
],
);
// Usage
addTodo("New todo");Creates a context provider and hooks for a Loro Mirror store.
const {
LoroContext,
LoroProvider,
useLoroContext,
useLoroState,
useLoroSelector,
useLoroAction,
} = createLoroContext(schema);Provider component for the Loro Mirror context.
<LoroProvider
doc={loroDoc}
initialState={initialState}
validateUpdates={true}
debug={false}
>
{children}
</LoroProvider>Hook to access the Loro Mirror store from context.
const store = useLoroContext();Hook to access and update the full state.
const [state, setState] = useLoroState();Hook to select a specific value from the state.
const todos = useLoroSelector((state) => state.todos);Hook to create an action that updates the state.
const addTodo = useLoroAction(
(state, text) => {
state.todos.push({ text, completed: false }); // `$cid` comes from Loro container id
},
[/* dependencies */]
);
// Usage
addTodo('New todo');$cidis always available onLoroMapstate and mirrors the underlying Loro container id.- Use
$cidfor Reactkeyand as the listidSelectorfor stable identity across edits and moves:schema.LoroList(item, x => x.$cid).
For high-frequency temporary changes (dragging, resizing), pass ephemeralStore to useLoroStore or LoroProvider. This changes how setState works: eligible changes (primitive values on existing Map keys) are automatically routed to EphemeralStore instead of LoroDoc. No separate update function is needed.
import { EphemeralStore } from "loro-crdt";
const eph = new EphemeralStore();
const { state, setState, finalizeEphemeralPatches } =
useLoroStore({ doc, schema: mySchema, ephemeralStore: eph });
// During drag — x is a primitive on an existing key → EphemeralStore
// No LoroDoc history is created for intermediate positions.
setState(
(s) => { s.items[i].x = e.clientX; },
{ finalizeTimeout: 1_000 }, // auto-commit after 1s of inactivity
);
// On mouseup — commit ephemeral values to LoroDoc
finalizeEphemeralPatches();Without ephemeralStore, the same setState call writes everything to LoroDoc as usual.
With createLoroContext, the provider accepts ephemeralStore and useLoroFinalizeEphemeral() returns a callback that commits pending ephemeral patches. See the core package README for routing rules.
MIT