Skip to main content
Install the React package alongside the core SDK:
npm install meridian-react meridian-sdk effect react
# or
pnpm add meridian-react meridian-sdk effect react
# or
bun add meridian-react meridian-sdk effect react

Setup

Create a client and pass it to MeridianProvider:
import { Effect } from "effect";
import { MeridianClient } from "meridian-sdk";
import { MeridianProvider } from "meridian-react";

const client = await Effect.runPromise(
  MeridianClient.create({
    url: "ws://localhost:3000",
    namespace: "my-app",
    token: process.env.MERIDIAN_TOKEN!,
  })
);

function App() {
  return (
    <MeridianProvider client={client}>
      <YourApp />
    </MeridianProvider>
  );
}
The provider closes the client automatically on unmount. All hooks in the tree share the same connection.

Hooks

useGCounter

const { value, increment } = useGCounter("gc:page-views");

<button onClick={() => increment()}>+1</button>
<span>{value}</span>

usePNCounter

const { value, increment, decrement } = usePNCounter("pn:votes");

useORSet

Define the schema outside the component so its reference is stable:
import { Schema } from "effect";

const Task = Schema.Struct({ id: Schema.String, title: Schema.String });

function TaskList() {
  const { elements, add, remove } = useORSet("or:tasks", Task);
}

useLwwRegister

const TitleSchema = Schema.String;

function Title() {
  const { value, set } = useLwwRegister("lw:doc-title", TitleSchema);
}

usePresence

Define the schema outside the component. Pass data to enable auto-heartbeat:
const CursorSchema = Schema.Struct({ x: Schema.Number, y: Schema.Number });

function Cursors() {
  const { online } = usePresence("pr:cursors", {
    schema: CursorSchema,
    data: { x: mouseX, y: mouseY },
    ttlMs: 5_000,
  });

  return online.map((entry) => (
    <Cursor key={entry.clientId} x={entry.data.x} y={entry.data.y} />
  ));
}
If data is omitted, the hook subscribes without sending a heartbeat (observe-only).

useAwareness

Ephemeral pub/sub for cursors, selections, and real-time UI state. Updates are fanned out in real time but not persisted. Define the schema outside the component for a stable reference:
import { Schema } from "effect";

const CursorSchema = Schema.Struct({ x: Schema.Number, y: Schema.Number });

function Canvas() {
  const { peers, update, clear } = useAwareness("cursors", CursorSchema);

  return (
    <div
      onMouseMove={(e) => update({ x: e.clientX, y: e.clientY })}
      onMouseLeave={() => clear()}
    >
      {peers.map((entry) => (
        <Cursor key={entry.clientId} x={entry.data.x} y={entry.data.y} />
      ))}
    </div>
  );
}
peers excludes the current client. For an accurate visitor count, combine with usePresence:
const { peers } = useAwareness("cursors", CursorSchema);
const { online } = usePresence("visitors", { data: {}, ttlMs: 15_000 });

// online.length = exact number of connected clients (including self)
See Awareness for full API reference.

useCRDTMap

const { value, lwwSet, incrementCounter } = useCRDTMap("doc:meta");

lwwSet("theme", "dark");
incrementCounter("views");

usePendingOpCount

Returns the number of operations buffered locally, waiting to be sent on reconnect. Use it to build a “syncing” indicator:
import { usePendingOpCount } from "meridian-react";

function SyncIndicator() {
  const pending = usePendingOpCount();
  if (pending === 0) return null;
  return <span>{pending} change{pending > 1 ? "s" : ""} pending...</span>;
}
The count is non-zero while disconnected with pending writes, and drops to zero once the connection is restored and all ops are flushed.

useMeridianClient

Access the underlying MeridianClient directly:
import { useMeridianClient } from "meridian-react";

function DebugPanel() {
  const client = useMeridianClient();
  return <pre>{JSON.stringify(client.claims)}</pre>;
}

Requirements

  • React 19+
  • meridian-sdk 0.3+