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

@cratebase/react

v0.5.0

Published

First-party React hooks for @cratebase/client: a CratebaseProvider/useCratebase context, typed useRecords/useRecord/useInfiniteRecords/useMutation/useAuth/usePresence/useSubscription, and a createCratebaseHooks<Schema>() factory for collection-name-to-rec

Readme

@cratebase/react

First-party React hooks for @cratebase/client, Cratebase's typed TypeScript SDK. This package is a thin, optional layer on top of the SDK: it does not wrap or replace anything, it exposes CratebaseClient's existing primitives (AuthStore.onChange, CollectionService.subscribe, presence.track) as idiomatic hooks so a React app doesn't have to hand-roll the same useEffect/subscribe/unsubscribe/id-merge pattern every screen ends up needing.

A project that only uses @cratebase/client directly (server code, a non-React frontend, a React app that prefers its own data layer) never installs @cratebase/react and pays zero cost.

bun add @cratebase/client @cratebase/react react

@cratebase/client (pinned to ^0.1.0, the version this package was built and verified against) and react (^18 || ^19) are peer dependencies.

Two ways to use these hooks

Explicit client — every hook takes a CratebaseClient as its first argument, no provider needed:

import { createClient } from "@cratebase/client";

export const cb = createClient("http://127.0.0.1:8090");
import { useRecords } from "@cratebase/react";
import { cb } from "./cratebase";

function PostList() {
  const { records: posts, loading, error } = useRecords(cb, "posts", { sort: "-created" });
  if (loading) return <p>Loading…</p>;
  return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}

CratebaseProvider — wrap your app once, then call the same hooks without a client:

import { CratebaseProvider } from "@cratebase/react";
import { cb } from "./cratebase";

export function App({ children }: { children: React.ReactNode }) {
  return <CratebaseProvider client={cb}>{children}</CratebaseProvider>;
}
import { useRecords } from "@cratebase/react";

function PostList() {
  const { records: posts } = useRecords("posts", { sort: "-created" });
  // ...
}

Every hook resolves its client from whichever it's given — an explicit first argument, falling back to CratebaseProvider context — via a shared internal useResolvedClient (useContext is always called, so branching on its result afterwards never breaks the rules of hooks). Both styles work in the same app.

Next.js App Router: CratebaseProvider and every hook here use React context and useSyncExternalStore, so anything that renders them needs a "use client" boundary — typically a small <Providers> client component wrapping CratebaseProvider, rendered from a server layout. Nothing in this package touches window/document at import time.

Typed collections: createCratebaseHooks<Schema>()

Generate a Schema with @cratebase/schema-codegen (the same one createClient<Schema>() takes) and pass it once to createCratebaseHooks for collection-name-to-record-type inference on every hook:

// cratebase.ts
import { createClient } from "@cratebase/client";
import { createCratebaseHooks } from "@cratebase/react";
import type { Schema } from "./cratebase-types";

export const cb = createClient<Schema>("http://localhost:8090");
export const { useRecords, useRecord, useInfiniteRecords, useMutation, useAuth, useCratebase } =
  createCratebaseHooks<Schema>();
import { useRecords } from "./cratebase";

const { records: posts } = useRecords("posts", { sort: "-created" }); // typed from Schema["posts"]

Why a factory, not module augmentation: augmenting a global interface only supports one schema per process — awkward for tests (a fixture schema would leak across test files) and for any app with more than one client/schema. A factory keeps the schema an ordinary type parameter, at the cost of one extra call site, the same trade zustand's and jotai's typed-store factories make. The hooks it returns still read their client from CratebaseProvider context (or take one explicitly) — this only narrows types.

type, not interface: Schema must be type Schema = { ... }, not interface Schema { ... }. TypeScript only infers an object type's implicit index signature — what lets it satisfy AnySchema's Record<string, Record<string, unknown>> constraint — for a type alias, never a named interface. This applies to createClient<Schema>() too.

useAuth — live auth state

Subscribes to client.auth via useSyncExternalStore, re-rendering on sign-in, sign-out, token refresh, or an out-of-band change (another tab writing to the same LocalAuthStore, or a direct client.auth.signOut() call). signIn/signOut are convenience pass-throughs to client.auth.signIn/client.auth.signOut; isLoading is true only for the SSR/first-render snapshot (every real auth store hydrates synchronously, so it's false on any client render).

import { useAuth } from "@cratebase/react";
import { cb } from "./cratebase";

function Nav() {
  const { user, isValid, isSuperuser, signIn, signOut } = useAuth(cb);
  if (!isValid) return <button onClick={() => signIn.password({ identity, password })}>Sign in</button>;
  return <span>{user?.email} {isSuperuser && "(admin)"} <button onClick={() => signOut()}>Sign out</button></span>;
}

useRecords — live list

Fetches with fullList (no page/perPage) or a single list() page (either given), then — when realtime is enabled (the default) — refetches on every matching realtime event, debounced to coalesce a burst of writes. Refetch, not local create/update/delete patching: a patched list has to reimplement the server's own filter/sort/pagination semantics client-side to stay correct, which quietly drifts; refetching keeps the list exactly what the server would return for the same query.

import { useRecords } from "@cratebase/react";
import { cb } from "./cratebase";

function PostList() {
  const { records: posts, loading, error, page, totalPages } = useRecords(cb, "posts", {
    sort: "-created",
    filter: "published = true",
    page: 1,
    perPage: 20,
  });
  // ...
}

Writes go through useMutation or client.collection("posts").create()/.update()/.delete() directly — this hook does not wrap writes.

useRecord — live single record

The single-record variant of useRecords: fetches once with one(), then subscribes to that record's own topic (so the server only pushes events for this one row) and applies the event directly rather than refetching.

import { useRecord } from "@cratebase/react";
import { cb } from "./cratebase";

function PostDetail({ postId }: { postId: string }) {
  const { record: post, loading, deleted } = useRecord(cb, "posts", postId);
  if (deleted) return <p>This post was deleted.</p>;
  return <h1>{post?.title}</h1>;
}

useInfiniteRecords — load more

Accumulates pages into one growing records array; loadMore() fetches the next one. Realtime defaults to false (unlike useRecords) — refetching from page 1 on every event would silently reset however many pages the caller has already loaded. Pass realtime: true to opt in anyway.

const { records, loading, loadingMore, hasMore, loadMore } = useInfiniteRecords(cb, "posts", { perPage: 20 });

useMutation — create/update/remove with optimistic UI

pending/error state, plus an optional { apply, rollback } pair per call for optimistic UI — apply() runs before the request is sent, rollback() only if it throws. Decoupled from any particular list's state; wire it up to whatever you're showing optimistically.

const { create, update, remove, pending, error } = useMutation(cb, "posts");

await create(
  { title },
  { optimistic: { apply: () => setLocal((p) => [...p, draft]), rollback: () => setLocal((p) => p.filter((x) => x !== draft)) } },
);

useChannel — a realtime channel, not tied to any record

Subscribes to channel:<name> for the component's lifetime and exposes a bound publish. Gated server-side by a _channels row (subscribeRule/publishRule; no matching row disables the channel by default) — see the channels & presence docs.

const { lastMessage, publish, connected } = useChannel<{ text: string }>(cb, "room1", {
  onMessage: (m) => console.log(m.event, m.data), // fires for every message, not just the latest
});

await publish("chat", { text: "hi" });

usePresence — who's on a channel right now

Heartbeats state on a realtime channel for as long as the component is mounted and state isn't null, and keeps a live members list. Server-authoritative: a member is removed automatically when its connection drops or after ~45s with no heartbeat — no staleness window to tune.

const { members, loading } = usePresence(cb, "room1", { name: user.name, cursor });
// pass `null` as `state` to observe without tracking your own presence

This is a different hook from the pre-realtime-channels useRecordPresence (this package's old usePresence — a client-side heartbeat + realtime + staleness-window pattern over an ordinary collection, still exported under its new name, still works, just no longer named usePresence now that this name means the server-authoritative version above):

const { online, loading } = useRecordPresence(cb, "presence", { userRef: user.id });

Prefer usePresence/useChannel for anything new — no backing collection to create. Reach for useRecordPresence only if you already have a natural "peers" collection you'd rather use as the source of truth.

useNotifications — in-app notification bell

Live list of the signed-in caller's own _notifications, plus unread count and the two bulk actions a bell needs — refetches on realtime event (debounced), same shape as useRecords.

const { items, unreadCount, markRead, markAllRead } = useNotifications(cb);

Sending (cb.notifications.send(...), superuser/API key only) and reading are independent — a frontend typically only ever calls this hook; sending happens server-side via $notify.send(...) in a JS hook or from trusted backend code.

useUpload — presigned direct uploads with progress

Wraps client.files.upload (presign, then PUT straight to storage) with progress/pending/ error state and an abort(), instead of every caller wiring up its own useState around the same onProgress callback:

const { upload, progress, pending } = useUpload(cb);

async function onFileChange(file: File) {
  const result = await upload(file, { collection: "posts", field: "cover" });
  await cb.collection("posts").create({ id: result.recordId, title, cover: result.token });
}

Composes with a caller-supplied onProgress/signal rather than overriding them.

useSubscription — raw realtime events

A thin, stateless primitive for callers who want raw realtime events without useRecords'/ useRecord's list-merging opinion.

useSubscription(cb, "posts", "*", (event) => {
  if (event.action === "create") toast(`New post: ${event.record.title}`);
});

useMagicLinkCallback — finish a magic-link sign-in

Mount this on whatever page your magic-link email points at (its redirectUrl, or authOptions.magicLink.urlTemplate's default). It reads the token query param (getMagicLinkTokenFromUrl under the hood), calls client.auth.signIn.magicLink({ token }), and removes the param from the URL on success so a refresh doesn't replay it.

import { useMagicLinkCallback } from "@cratebase/react";
import { cb } from "./cratebase";

function MagicLinkCallback() {
  const { status, error } = useMagicLinkCallback(cb, {
    onSuccess: () => navigate("/"),
  });
  if (status === "pending") return <p>Signing you in…</p>;
  if (status === "error") return <p>That link didn't work: {String(error)}</p>;
  return null;
}

status is "idle" (no token param present, e.g. a normal visit to this route) | "pending" | "success" | "error" | "none".

Versioning

This package versions independently from the cratebase server binary and from @cratebase/client. It's an optional React add-on with its own semver lifecycle — the @cratebase/client peer dependency it targets, not the server release cadence, is what actually constrains breaking changes here. Publishing is triggered by pushing a react-v<version> tag (see .github/workflows/publish-react.yml), kept deliberately separate from the server's own v* release tags and from client-v*, so a server or client release never forces an unrelated hooks publish, and a hooks hotfix never waits on one.

Development

This package is part of the root bun workspace (sdk/js/client, sdk/js/react, …), so bun install at the repo root sets it up too.

bun install
bun run --cwd sdk/js/react typecheck   # tsc --noEmit + the type-inference fixture
bun run --cwd sdk/js/react test        # bun test (happy-dom + @testing-library/react)
bun run --cwd sdk/js/react build       # emit dist/

bun run sdk:check from the repo root runs all three SDK packages' checks in one command (also wired into CI).