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 🙏

© 2026 – Pkg Stats / Ryan Hefner

meridian-react

v1.4.1

Published

React hooks for Meridian CRDT server

Downloads

73

Readme

meridian-react

React hooks for Meridian — a CRDT server with live sync over WebSocket.

Installation

bun add meridian-react
# or
npm install meridian-react

Requires react ^19.0.0, meridian-sdk ^1.1.0, and effect ^3.21.0 as peer dependencies.

Setup

MeridianProvider takes a MeridianClient instance created via MeridianClient.create():

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 client is automatically closed when the provider unmounts.

Hooks

useGCounter

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

usePNCounter

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

useORSet

import { Schema } from "effect";

// Define schema outside the component for a stable reference
const Task = Schema.Struct({ id: Schema.String, title: Schema.String });

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

useLwwRegister

import { Schema } from "effect";

const TitleSchema = Schema.String;

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

usePresence

Takes an optional opts object. When opts.data is provided, heartbeats are sent automatically and leave() is called on unmount.

import { Schema } from "effect";

// Define schema outside the component for a stable reference
const CursorSchema = Schema.Struct({ x: Schema.Number, y: Schema.Number });

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

| Option | Type | Description | |--------|------|-------------| | schema | Schema<T>? | Decode peer data at runtime | | data | T? | Data to broadcast — triggers auto-heartbeat | | ttlMs | number? | Entry lifetime in ms (default: 30 000) | | heartbeatIntervalMs | number? | Override heartbeat send interval |

useAwareness

Ephemeral pub/sub channel for high-frequency transient state (cursors, selections, "is typing"). Updates are fanned out in real time but not persisted — new peers won't see your state until you send another update.

import { Schema } from "effect";

// Define schema outside the component for a stable reference
const CursorSchema = Schema.Struct({ x: Schema.Number, y: Schema.Number });

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

// peers: AwarenessEntry<{ x, y }>[] — other clients only, self excluded
// update({ x, y }) — send our position
// clear()         — remove our entry (e.g. on mouse leave)

| Option | Type | Description | |--------|------|-------------| | key | string | Awareness channel name (e.g. "cursors", "selection:doc-1") | | schema | Schema<T>? | Decode peer payloads at runtime |

peers excludes the current client. Use peers.length + 1 for a total visitor count — or combine with usePresence for an accurate count that includes clients who haven't moved yet.

useRGA

Collaborative text editing — ordered sequence CRDT.

const { value, insert, delete: del } = useRGA("rg:doc-123");

useTree

Collaborative hierarchical tree — outlines, mind maps, document trees.

const { roots, addNode, moveNode, updateNode, deleteNode } = useTree("tr:outline");

// Add root node
const rootId = addNode(null, "a0", "Introduction");
// Add child
const childId = addNode(rootId, "a0", "Chapter 1");
// Move, update, delete
moveNode(childId, null, "b0");
updateNode(childId, "Chapter 1 — Updated");
deleteNode(childId);

roots is an array of TreeNodeValue{ id, value, children: TreeNodeValue[] }. Concurrent moves converge via Kleppmann et al. (2021) — cycle-creating moves are silently discarded.

useCRDTMap

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

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

useQuery

One-shot cross-CRDT query over HTTP. Re-runs when spec changes.

import { useMemo } from "react";
import { useQuery } from "meridian-react";

function TotalViews() {
  const spec = useMemo(() => ({ from: "gc:views-*", aggregate: "sum" as const }), []);
  const { data, loading, error } = useQuery(spec);

  if (loading) return <span>Loading…</span>;
  return <span>Total: {String(data?.value)}</span>;
}

useLiveQuery

Reactive WebSocket subscription — the server pushes a new result on every matching CRDT delta. Subscribes on mount, unsubscribes on unmount.

import { useMemo } from "react";
import { useLiveQuery } from "meridian-react";

function LiveTotal() {
  const spec = useMemo(() => ({ from: "gc:views-*", aggregate: "sum" as const }), []);
  const { data, loading, error } = useLiveQuery(spec);

  if (loading) return <span>Connecting…</span>;
  return <span>Live views: {String(data?.value)}</span>;
}

Stabilize spec with useMemo — a new object reference re-subscribes. The SDK re-sends the subscription automatically after a WebSocket reconnect.

| | useQuery | useLiveQuery | |---|---|---| | Transport | HTTP POST | WebSocket push | | Updates on | spec change | every matching delta | | Use case | one-shot reads | live dashboards, reactive aggregates |

usePendingOpCount

Returns the number of operations buffered locally, waiting to be sent on reconnect. Useful for building a "syncing" or "offline" 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>;
}

useMeridianClient

Access the underlying MeridianClient directly when needed:

import { useMeridianClient } from "meridian-react";

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

Requirements

  • React 19+
  • meridian-sdk 1.1+