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

@waits/lively-react

v0.0.1

Published

React hooks for Lively real-time collaboration

Readme

@waits/lively-react

React hooks and providers for Lively real-time collaboration.

Install

bun add @waits/lively-react

Requires react >= 18 as a peer dependency.


Setup

import { LivelyClient } from "@waits/lively-client";
import { LivelyProvider, RoomProvider } from "@waits/lively-react";

// Create once at module level — not inside a component
const client = new LivelyClient({ serverUrl: "ws://localhost:2001" });

function App() {
  return (
    <LivelyProvider client={client}>
      <RoomProvider
        roomId="my-room"
        userId={currentUser.id}
        displayName={currentUser.name}
        initialStorage={{ count: 0 }}
      >
        <Canvas />
      </RoomProvider>
    </LivelyProvider>
  );
}

Providers

<LivelyProvider>

Makes a shared LivelyClient available to all nested hooks. Must wrap <RoomProvider>.

interface LivelyProviderProps {
  client: LivelyClient;
  children: ReactNode;
}

<RoomProvider>

Joins a room and exposes it to child hooks. Creates the room synchronously on first render; leaves on unmount.

interface RoomProviderProps {
  roomId: string;
  userId: string;
  displayName: string;
  initialStorage?: Record<string, unknown>; // written on first connect if storage is empty
  cursorThrottleMs?: number;                // default: 50
  inactivityTime?: number;                  // ms before marking user as away
  offlineInactivityTime?: number;           // ms before marking user as offline
  location?: string;                        // location identifier for this client
  presenceMetadata?: Record<string, unknown>; // arbitrary presence metadata
  token?: string;                           // auth token sent as query param on WS connect
  children: ReactNode;
}

Hooks

useStatus

function useStatus(): "connecting" | "connected" | "reconnecting" | "disconnected"

Returns the current WebSocket connection status. Re-renders on every status change.

const status = useStatus();
if (status !== "connected") return <p>{status}</p>;

useLostConnectionListener

function useLostConnectionListener(callback: () => void): void

Fires callback once when the connection drops from "connected" to "reconnecting". Does not fire on intentional disconnect. Callback is stored in a ref — no need to memoize.

useLostConnectionListener(() => toast("Connection lost, reconnecting…"));

useSelf

function useSelf(): PresenceUser | null

Returns the current user's own presence data. Returns null before the first presence broadcast from the server.

interface PresenceUser {
  userId: string;
  displayName: string;
  color: string;
  connectedAt: number;
  onlineStatus: "online" | "away" | "offline";
  lastActiveAt: number;
  isIdle: boolean;
  avatarUrl?: string;
  location?: string;
  metadata?: Record<string, unknown>;
}
const self = useSelf();
if (!self) return null;
return <Avatar name={self.displayName} color={self.color} />;

useOthers

function useOthers(): PresenceUser[]

Returns all other users currently in the room. Re-renders only when the list changes (shallow-equal check).

const others = useOthers();
return <ul>{others.map(u => <li key={u.userId}>{u.displayName}</li>)}</ul>;

useOther

function useOther<T = PresenceUser>(
  userId: string,
  selector?: (u: PresenceUser) => T
): T | null

Returns a single other user by userId, optionally transformed by selector. Returns null if the user is not in the room.

const name = useOther("user-abc", u => u.displayName); // string | null
const user = useOther("user-abc");                      // PresenceUser | null

useOthersMapped

function useOthersMapped<T>(selector: (user: PresenceUser) => T): T[]

Maps all other users through selector. Re-renders only when the mapped output changes — more efficient than useOthers + a manual map when you only need a slice of each user.

const names = useOthersMapped(u => u.displayName);

useStorage

function useStorage<T>(selector: (root: LiveObject) => T): T | null

Reads a value from shared CRDT storage via a selector. Returns null while storage is loading (before the first storage:init from the server). Re-renders only when the selected value changes (shallow-equal check).

const count = useStorage(root => root.get("count") as number);
if (count === null) return <p>Loading…</p>;
return <p>Count: {count}</p>;

useMutation

function useMutation<Args extends unknown[], R>(
  callback: (ctx: { storage: { root: LiveObject } }, ...args: Args) => R,
  deps: unknown[]
): (...args: Args) => R

Returns a stable callback that mutates shared storage inside a room.batch(). Throws if called before storage has loaded — wait until useStorage returns a non-null value before triggering mutations.

const increment = useMutation(({ storage }) => {
  const count = storage.root.get("count") as number;
  storage.root.set("count", count + 1);
}, []);

// With arguments:
const rename = useMutation(({ storage }, name: string) => {
  storage.root.set("title", name);
}, []);

useCursors

function useCursors(): Map<string, CursorData>

Returns a Map<userId, CursorData> of all cursor positions in the room (including the current user). Re-renders only when positions actually change.

interface CursorData {
  userId: string;
  displayName: string;
  color: string;
  x: number;
  y: number;
  lastUpdate: number;
  viewportPos?: { x: number; y: number };
  viewportScale?: number;
}
const cursors = useCursors();
return (
  <>
    {[...cursors.entries()].map(([userId, pos]) => (
      <Cursor key={userId} x={pos.x} y={pos.y} />
    ))}
  </>
);

useUpdateCursor

function useUpdateCursor(): (
  x: number,
  y: number,
  viewportPos?: { x: number; y: number },
  viewportScale?: number
) => void

Returns a stable function to broadcast the current user's cursor position. Coordinates should be relative to the container you want to track. Optional viewport params enable follow mode.

const updateCursor = useUpdateCursor();
<div onMouseMove={e => updateCursor(e.clientX, e.clientY)} />

useBroadcastEvent

function useBroadcastEvent<T extends { type: string }>(): (event: T) => void

Returns a stable function to broadcast a custom ephemeral event to all other users in the room. Events are not persisted. Pair with useEventListener on the receiving end.

const broadcast = useBroadcastEvent<{ type: "confetti" }>();
<button onClick={() => broadcast({ type: "confetti" })}>Celebrate</button>

useEventListener

function useEventListener<T extends Record<string, unknown>>(
  callback: (event: T) => void
): void

Subscribes to custom events broadcast by other users via useBroadcastEvent. The callback is stored in a ref and does not need to be memoized or included in dependency arrays.

useEventListener<{ type: "confetti" }>(event => {
  if (event.type === "confetti") triggerConfetti();
});

useOthersUserIds

function useOthersUserIds(): string[]

Returns a sorted array of userIds for all other users in the room. Only re-renders on join/leave, not on presence data changes. More efficient than useOthers when you only need identity.

const ids = useOthersUserIds();
return <>{ids.map(id => <Cursor key={id} userId={id} />)}</>;

useMyPresence

function useMyPresence(): [PresenceUser | null, (data: Partial<PresenceUpdatePayload>) => void]

Convenience wrapper combining useSelf() and useUpdateMyPresence() into a single [self, update] tuple.

const [me, updatePresence] = useMyPresence();
if (me) updatePresence({ location: "settings" });

useUpdateMyPresence

function useUpdateMyPresence(): (data: Partial<PresenceUpdatePayload>) => void

Returns a stable function to update the current user's presence data (location, metadata, onlineStatus).

const updatePresence = useUpdateMyPresence();
updatePresence({ location: "page-1" });

useOthersListener

function useOthersListener(callback: (event: OthersEvent) => void): void

Fires a callback whenever another user enters, leaves, or updates their presence. The callback receives a discriminated union: { type: "enter" | "leave" | "update"; user: PresenceUser; others: PresenceUser[] }. Uses callbackRef pattern -- no stale closures.

useOthersListener(event => {
  if (event.type === "enter") toast(`${event.user.displayName} joined`);
});

useBatch

function useBatch(): <T>(fn: () => T) => T

Returns a stable function that wraps its callback in room.batch(). Batched mutations combine into a single history entry and one network message.

const batch = useBatch();
batch(() => {
  root.set("x", 1);
  root.set("y", 2);
});

useObject

function useObject<T extends Record<string, unknown>>(key: string): LiveObject<T> | null

Returns a LiveObject<T> stored at the given top-level storage key, or null while loading.

const settings = useObject<{ theme: string }>("settings");
// Read: settings?.get("theme")

useMap

function useMap<V>(key: string): LiveMap<string, V> | null

Returns a LiveMap<string, V> stored at the given top-level storage key, or null while loading.

const users = useMap<UserData>("users");
// Read: users?.get("u1")

useList

function useList<T>(key: string): LiveList<T> | null

Returns a LiveList<T> stored at the given top-level storage key, or null while loading.

const items = useList<string>("items");
// Read: items?.toArray()

useErrorListener

function useErrorListener(callback: (error: Error) => void): void

Fires a callback when a WebSocket-level error occurs on the room connection. Uses callbackRef pattern.

useErrorListener(err => console.error("Room error:", err.message));

useSyncStatus

function useSyncStatus(): "synchronized" | "synchronizing" | "not-synchronized"

Returns a high-level sync status derived from connection state and storage loading. "synchronized" = connected + storage loaded, "synchronizing" = connecting/loading, "not-synchronized" = disconnected.

const sync = useSyncStatus();
if (sync === "not-synchronized") return <OfflineBanner />;

useOthersOnLocation

function useOthersOnLocation(locationId: string): PresenceUser[]

Returns other users at a specific location. Subscribes to presence and filters by location field.

const viewers = useOthersOnLocation("page-1");
return <p>{viewers.length} others on this page</p>;

usePresenceEvent

function usePresenceEvent(
  event: "stateChange",
  callback: (user: PresenceUser, prevStatus: string, newStatus: string) => void
): void

Fires callback when another user's onlineStatus changes. Useful for detecting away/offline transitions.

usePresenceEvent("stateChange", (user, prev, next) => {
  console.log(`${user.displayName}: ${prev} → ${next}`);
});

useLiveState

function useLiveState<T>(
  key: string,
  initialValue: T,
  opts?: { syncDuration?: number }
): [T, (value: T | ((prev: T) => T)) => void]

Like useState but shared across all room participants. Ephemeral key-value state synced via the room. syncDuration controls debounce in ms (default: 50).

const [color, setColor] = useLiveState("bgColor", "#fff");
<button onClick={() => setColor("#f00")}>Red</button>

useLiveStateData

function useLiveStateData<T>(key: string): T | undefined

Read-only subscription to a live state key. Returns undefined if no value has been set.

const color = useLiveStateData<string>("bgColor");

useSetLiveState

function useSetLiveState<T>(key: string): (value: T, opts?: { merge?: boolean }) => void

Returns a stable setter function for a live state key. Write-only counterpart to useLiveStateData.

const setColor = useSetLiveState<string>("bgColor");
setColor("#0f0");

useUndo

function useUndo(): () => void

Returns a stable callback that triggers undo on the room's storage history.

const undo = useUndo();
<button onClick={undo}>Undo</button>

useRedo

function useRedo(): () => void

Returns a stable callback that triggers redo on the room's storage history.

const redo = useRedo();
<button onClick={redo}>Redo</button>

useCanUndo / useCanRedo

function useCanUndo(): boolean
function useCanRedo(): boolean

Returns whether undo/redo is available. Re-renders when this changes.

const canUndo = useCanUndo();
<button disabled={!canUndo} onClick={undo}>Undo</button>

useHistory

function useHistory(): {
  undo: () => void;
  redo: () => void;
  canUndo: boolean;
  canRedo: boolean;
}

Combined hook returning all undo/redo utilities in a single object.

const { undo, redo, canUndo, canRedo } = useHistory();

useFollowUser

function useFollowUser(opts?: UseFollowUserOptions): UseFollowUserReturn

Follow mode hook. Tracks follow relationships via presence metadata (visible to all clients). Auto-exits follow mode when the target disconnects or the user interacts (wheel/pointerdown).

interface UseFollowUserOptions {
  onViewportChange?: (pos: { x: number; y: number }, scale: number) => void;
  exitOnInteraction?: boolean; // default: true
  onAutoExit?: (reason: "disconnected" | "interaction") => void;
}

interface UseFollowUserReturn {
  followingUserId: string | null;
  followUser: (userId: string) => void;
  stopFollowing: () => void;
  followers: string[];       // userIds following you
  isBeingFollowed: boolean;
}
const { followingUserId, followUser, stopFollowing, followers } = useFollowUser({
  onViewportChange: (pos, scale) => panTo(pos, scale),
  onAutoExit: (reason) => toast(`Stopped following (${reason})`),
});

return (
  <>
    {others.map(u => (
      <button key={u.userId} onClick={() => followUser(u.userId)}>
        Follow {u.displayName}
      </button>
    ))}
    {followingUserId && <button onClick={stopFollowing}>Stop following</button>}
  </>
);

createRoomContext

function createRoomContext<
  TPresence extends Record<string, unknown>,
  TStorage extends Record<string, unknown>,
>(): { RoomProvider, useStorage, useSelf, useMyPresence, ... }

Factory that returns typed versions of all hooks scoped to your app's presence and storage types. Zero runtime overhead -- just narrows generics.

type Presence = { cursor: { x: number; y: number } | null };
type Storage = { count: number; items: LiveList<string> };

const {
  RoomProvider,
  useStorage,
  useSelf,
  useMyPresence,
} = createRoomContext<Presence, Storage>();

ClientSideSuspense

<ClientSideSuspense fallback={<Spinner />}>
  {() => <CollaborativeEditor />}
</ClientSideSuspense>

SSR-safe Suspense wrapper. Renders fallback during SSR and the initial client render, then renders children() inside <Suspense>. children is a render function so hooks inside are not evaluated during SSR.

interface ClientSideSuspenseProps {
  children: () => ReactNode;
  fallback: ReactNode;
}

useRoom

function useRoom(): Room

Returns the raw Room instance from the nearest <RoomProvider>. Throws if used outside a provider. Useful for advanced patterns or direct access to room.batch(), room.send(), etc.


useClient

function useClient(): LivelyClient

Returns the LivelyClient from the nearest <LivelyProvider>. Useful for managing multiple rooms outside of React lifecycle.


useIsInsideRoom

function useIsInsideRoom(): boolean

Returns true if the component is inside a <RoomProvider>. Useful for conditional rendering of collaboration features.


useStorageRoot

function useStorageRoot(): { root: LiveObject } | null

Returns the raw storage root object, or null while storage is loading. Lower-level than useStorage — prefer useStorage(selector) for reactive reads.


Suspense entry point

Import from @waits/lively-react/suspense to use useStorageSuspense — a variant of useStorage that throws a promise instead of returning null while loading. Wrap the consuming component in <Suspense>.

import { useStorageSuspense } from "@waits/lively-react/suspense";

// Inside a <Suspense fallback={<p>Loading…</p>}> boundary:
function Canvas() {
  const count = useStorageSuspense(root => root.get("count") as number);
  // count is always T here — never null
  return <p>Count: {count}</p>;
}

Suspense variants of the CRDT shortcut hooks are also available:

import {
  useStorageSuspense,
  useObjectSuspense,
  useMapSuspense,
  useListSuspense,
} from "@waits/lively-react/suspense";

const settings = useObjectSuspense<{ theme: string }>("settings");
const users = useMapSuspense<UserData>("users");
const items = useListSuspense<string>("items");

The suspense entry re-exports all other hooks for single-import convenience — you do not need to import from both entry points.

useStorageSuspense cannot be used during SSR. It will throw if a server snapshot is requested.


CRDT types

LiveObject, LiveMap, and LiveList are re-exported from this package for convenience:

import { LiveObject, LiveMap, LiveList } from "@waits/lively-react";