npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@loro-extended/react

v0.7.0

Published

React hooks and utilities for Loro

Readme

@loro-extended/react

React hooks for building real-time collaborative applications with Loro CRDT documents. Offers both simple untyped and schema-based typed APIs.

What This Package Does

This package provides React-specific bindings for Loro CRDT documents with two approaches:

  • Simple API: Direct LoroDoc access without schema dependencies
  • Typed API: Schema-based documents with type safety and empty state management

Key Features

  • Document Lifecycle: Automatic loading, creation, and synchronization of documents
  • React Integration: Reactive hooks that re-render when documents change
  • Flexible APIs: Choose between simple or typed approaches based on your needs
  • Type Safety: Full TypeScript support (optional schema-driven type inference)
  • Loading States: Handle sync status separately from data availability

Installation

For Simple API (no schema dependencies)

npm install @loro-extended/react @loro-extended/repo loro-crdt
# or
pnpm add @loro-extended/react @loro-extended/repo loro-crdt

For Typed API (with schema support)

npm install @loro-extended/react @loro-extended/change @loro-extended/repo loro-crdt
# or
pnpm add @loro-extended/react @loro-extended/change @loro-extended/repo loro-crdt

Quick Start

Simple API (Untyped)

For direct LoroDoc access without schema dependencies:

import { useUntypedDocument } from "@loro-extended/react";

interface TodoDoc {
  title: string;
  todos: Array<{ id: string; text: string; completed: boolean }>;
}

function SimpleTodoApp() {
  const [doc, changeDoc, handle] = useUntypedDocument("todo-doc");

  // Check if doc is ready before using
  if (!doc) {
    return <div>Loading...</div>;
  }

  const data = doc.toJSON() as TodoDoc;

  return (
    <div>
      <h1>{data.title || "My Todo List"}</h1>

      <button
        onClick={() =>
          changeDoc((doc) => {
            const titleText = doc.getText("title");
            titleText.insert(0, "📝 ");
          })
        }
      >
        Add Emoji
      </button>

      {(data.todos || []).map((todo, index) => (
        <div key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() =>
              changeDoc((doc) => {
                const todosList = doc.getList("todos");
                const todoMap = todosList.get(index);
                if (todoMap) {
                  todoMap.set("completed", !todo.completed);
                }
              })
            }
          />
          {todo.text}
        </div>
      ))}

      <button
        onClick={() =>
          changeDoc((doc) => {
            const todosList = doc.getList("todos");
            todosList.push({
              id: Date.now().toString(),
              text: "New Todo",
              completed: false,
            });
          })
        }
      >
        Add Todo
      </button>
    </div>
  );
}

Typed API (Schema-based)

For schema-aware documents with type safety and empty state management:

import { useDocument, Shape } from "@loro-extended/react";

// Define your document schema (see @loro-extended/change for details)
const todoSchema = Shape.doc({
  title: Shape.text(),
  todos: Shape.list(
    Shape.plain.object({
      id: Shape.plain.string(),
      text: Shape.plain.string(),
      completed: Shape.plain.boolean(),
    })
  ),
});

// Define empty state (default values)
const emptyState = {
  title: "My Todo List",
  todos: [],
};

function TypedTodoApp() {
  const [doc, changeDoc, handle] = useDocument(
    "todo-doc",
    todoSchema,
    emptyState
  );

  // doc is ALWAYS defined - no loading check needed!
  return (
    <div>
      <h1>{doc.title}</h1>

      <button
        onClick={() =>
          changeDoc((draft) => {
            draft.title.insert(0, "📝 ");
          })
        }
      >
        Add Emoji
      </button>

      {doc.todos.map((todo, index) => (
        <div key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() =>
              changeDoc((draft) => {
                // Update the specific todo item
                const todoItem = draft.todos.get(index);
                if (todoItem) {
                  todoItem.completed = !todo.completed;
                }
              })
            }
          />
          {todo.text}
        </div>
      ))}

      <button
        onClick={() =>
          changeDoc((draft) => {
            draft.todos.push({
              id: Date.now().toString(),
              text: "New Todo",
              completed: false,
            });
          })
        }
      >
        Add Todo
      </button>
    </div>
  );
}

Core Hooks

useUntypedDocument - Simple API

For direct LoroDoc access without schema dependencies.

Signature

function useUntypedDocument<T = any>(
  documentId: string
): [
  doc: LoroDoc | null,
  changeDoc: (fn: SimpleChangeFn) => void,
  handle: DocHandle | null
];

Parameters

  • documentId: Unique identifier for the document

Returns

  1. doc: LoroDoc | null - The raw LoroDoc instance

    • null when not ready (requires loading check)
    • Direct access to all LoroDoc methods when available
    • Use doc.toJSON() to get plain JavaScript object
  2. changeDoc: (fn: SimpleChangeFn) => void - Function to modify the document

    • Provides direct access to LoroDoc for mutations
    • Example: changeDoc(doc => doc.getText("title").insert(0, "Hello"))
  3. handle: DocHandle | null - The document handle

    • Provides access to sync state (via readyStates) and events
    • null initially, then becomes available

useDocument - Typed API

For schema-aware documents with type safety and empty state management.

Signature

function useDocument<T extends DocShape>(
  documentId: string,
  schema: T,
  emptyState: InferPlainType<T>
): [
  doc: InferPlainType<T>,
  changeDoc: (fn: ChangeFn<T>) => void,
  handle: DocHandle | null
];

Parameters

  • documentId: Unique identifier for the document
  • schema: Document schema (see @loro-extended/change for schema documentation)
  • emptyState: Default values shown before/during sync

Returns

  1. doc: InferPlainType<T> - The current document state

    • Always defined due to empty state overlay
    • Shows empty state initially, then overlays CRDT data when available
    • Automatically re-renders when local or remote changes occur
  2. changeDoc: (fn: ChangeFn<T>) => void - Function to modify the document

    • Uses schema-aware draft operations
    • All changes are automatically committed and synchronized
    • See @loro-extended/change for operation details
  3. handle: DocHandle | null - The document handle

    • Provides access to sync state (via readyStates)
    • Emits events for state changes and document updates
    • null initially, then becomes available

usePresence - Typed Presence

For schema-aware presence with type safety and default values.

const { self, all, setSelf } = usePresence(
  documentId,
  presenceSchema,
  emptyPresence
);

// self: InferPlainType<PresenceSchema>
// all: Record<string, InferPlainType<PresenceSchema>>

useUntypedPresence - Untyped Presence

For presence without schema validation.

const { self, all, setSelf } = useUntypedPresence(documentId);

// self: any
// all: Record<string, any>

Choosing Between APIs

Use Simple API When:

  • You want minimal dependencies (no schema package required)
  • You prefer direct control over LoroDoc operations
  • You're building a simple application or prototype
  • You want to integrate with existing Loro code

Use Typed API When:

  • You want full type safety and IntelliSense support
  • You prefer declarative schema definitions
  • You want empty state management (no loading checks needed)
  • You're building a complex application with structured data

Key Benefits

Simple API Benefits

  • Minimal Dependencies: Only requires @loro-extended/react and loro-crdt
  • Direct Control: Full access to LoroDoc methods and operations
  • Flexibility: No schema constraints, work with any document structure
  • Performance: No schema transformation overhead

Typed API Benefits

  • 🚀 No Loading States for Data: doc is always defined due to empty state overlay
  • 🔄 Immediate Rendering: Components render immediately with empty state
  • 🎯 Type Safety: Full TypeScript support with compile-time validation
  • 🛡️ Schema Validation: Ensures data consistency across your application

Example: No Loading States

// Simple API - requires loading check
const [doc, changeDoc] = useUntypedDocument("doc-id");
if (!doc) return <div>Loading...</div>;

// Typed API - always available
const [doc, changeDoc] = useDocument("doc-id", schema, emptyState);
return <h1>{doc.title}</h1>; // Always works!

Setting Up the Repo Context

Wrap your app with RepoProvider to provide document synchronization:

import { RepoProvider } from "@loro-extended/react";
import { Repo } from "@loro-extended/repo";

// Configure your adapters (see @loro-extended/repo docs)
const config = {
  identity: { name: "user-1", type: "user" },
  adapters: [networkAdapter, storageAdapter],
};

function App() {
  return (
    <RepoProvider config={config}>
      <YourComponents />
    </RepoProvider>
  );
}

React-Specific Patterns

Multiple Documents

// Simple API
function MultiDocApp() {
  const [todos, changeTodos] = useUntypedDocument<TodoDoc>("todos");
  const [notes, changeNotes] = useUntypedDocument<NoteDoc>("notes");

  return (
    <div>
      {todos && <TodoList doc={todos.toJSON()} onChange={changeTodos} />}
      {notes && <NoteEditor doc={notes.toJSON()} onChange={changeNotes} />}
    </div>
  );
}

// Typed API
function MultiDocApp() {
  const [todos, changeTodos] = useDocument("todos", todoSchema, todoEmptyState);
  const [notes, changeNotes] = useDocument("notes", noteSchema, noteEmptyState);

  return (
    <div>
      <TodoList doc={todos} onChange={changeTodos} />
      <NoteEditor doc={notes} onChange={changeNotes} />
    </div>
  );
}

Conditional Document Loading

// Simple API
function ConditionalDoc({ documentId }: { documentId: string | null }) {
  const [doc, changeDoc] = useUntypedDocument(documentId || "default");

  if (!documentId) {
    return <div>Select a document</div>;
  }

  if (!doc) {
    return <div>Loading...</div>;
  }

  return <DocumentEditor doc={doc.toJSON()} onChange={changeDoc} />;
}

// Typed API
function ConditionalDoc({ documentId }: { documentId: string | null }) {
  const [doc, changeDoc] = useDocument(
    documentId || "default",
    schema,
    emptyState
  );

  if (!documentId) {
    return <div>Select a document</div>;
  }

  return <DocumentEditor doc={doc} onChange={changeDoc} />;
}

Custom Loading UI

function DocumentWithStatus() {
  const [doc, changeDoc, handle] = useDocument(id, schema, emptyState);

  // Check readyStates to determine sync status
  const isSyncing = handle?.readyStates.some(
    (s) => s.loading.state === "requesting"
  );

  return (
    <div>
      {isSyncing && <div className="status-bar">Syncing...</div>}
      <DocumentContent doc={doc} onChange={changeDoc} />
    </div>
  );
}

Performance

The hook uses React's useSyncExternalStore for optimal performance:

  • Only re-renders when the document actually changes
  • Efficient subscription management
  • Stable function references to prevent unnecessary re-renders

Type Safety

Full TypeScript support with automatic type inference:

// Types are automatically inferred from your schema
const [doc, changeDoc] = useDocument(documentId, schema, emptyState);
// doc: { title: string; todos: Array<{id: string; text: string; completed: boolean}> }
// changeDoc: (fn: (draft: DraftType) => void) => void

Complete Example

For a full collaborative React application, see the Todo SSE Example which demonstrates:

  • Setting up the Repo with network and storage adapters
  • Using useDocument for reactive document state
  • Building collaborative UI components
  • Handling offline scenarios

Advanced Usage

For advanced use cases, you can access the underlying building blocks:

import {
  useDocHandleState,
  useRawLoroDoc,
  useUntypedDocChanger,
  useTypedDocState,
  useTypedDocChanger,
} from "@loro-extended/react";

// Custom hook combining base components
function useCustomDocument(documentId: string) {
  const { handle } = useDocHandleState(documentId);
  const changeDoc = useUntypedDocChanger(handle);

  // Your custom logic here

  return [
    /* your custom return */
  ];
}

Requirements

  • React 18+
  • TypeScript 5+ (recommended)
  • A Repo instance from @loro-extended/repo

Optional Dependencies

  • @loro-extended/change - Required only for typed API (useDocument)

Related Packages

  • @loro-extended/change - Schema-based CRDT operations (optional)
  • @loro-extended/repo - Document synchronization and storage
  • Network adapters: @loro-extended/adapter-sse, @loro-extended/adapter-websocket, @loro-extended/adapter-webrtc, @loro-extended/adapter-http-polling
  • Storage adapters: @loro-extended/adapter-indexeddb, @loro-extended/adapter-leveldb, @loro-extended/adapter-postgres

License

MIT