Skip to content

Latest commit

 

History

History
293 lines (227 loc) · 6.84 KB

File metadata and controls

293 lines (227 loc) · 6.84 KB

Loro Mirror React

React integration for Loro Mirror - a state management library with Loro CRDT synchronization.

Installation

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-crdt

Usage

Basic Usage with Hooks

import 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...
}

Using Context Provider

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>
    );
}

API Reference

useLoroStore

Creates and manages a Loro Mirror store.

const { state, setState, finalizeEphemeralPatches, store } = useLoroStore({
  doc,
  schema,
  initialState,
  validateUpdates,
  debug,
});

Notes on updates:

  • setState from useLoroStore and the setter from useLoroState run synchronously; subsequent code can read the updated state immediately.
  • useLoroCallback and useLoroAction return synchronous functions that call setState under the hood.

useLoroValue

Subscribes to a specific value from a Loro Mirror store.

const todos = useLoroValue(store, (state) => state.todos);

useLoroCallback

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");

createLoroContext

Creates a context provider and hooks for a Loro Mirror store.

const {
    LoroContext,
    LoroProvider,
    useLoroContext,
    useLoroState,
    useLoroSelector,
    useLoroAction,
} = createLoroContext(schema);

LoroProvider

Provider component for the Loro Mirror context.

<LoroProvider
    doc={loroDoc}
    initialState={initialState}
    validateUpdates={true}
    debug={false}
>
    {children}
</LoroProvider>

useLoroContext

Hook to access the Loro Mirror store from context.

const store = useLoroContext();

useLoroState

Hook to access and update the full state.

const [state, setState] = useLoroState();

useLoroSelector

Hook to select a specific value from the state.

const todos = useLoroSelector((state) => state.todos);

useLoroAction

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');

$cid and list keys/selectors

  • $cid is always available on LoroMap state and mirrors the underlying Loro container id.
  • Use $cid for React key and as the list idSelector for stable identity across edits and moves: schema.LoroList(item, x => x.$cid).

Ephemeral Patches

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.

License

MIT