Kizaki
ReferenceTypeScript

@kizaki/react

React hooks for live queries, mutations, presence, and broadcast.

@kizaki/react wraps BrowserKizakiClient in hooks that handle subscriptions, loading states, cache invalidation, and cleanup.

KizakiProvider

Optional context provider that supplies a custom BrowserKizakiClient to all hooks in the tree. If omitted, hooks create and share a default client.

import { KizakiProvider } from "@kizaki/react";
import { createBrowserClient } from "@kizaki/sdk/browser";

const client = createBrowserClient();

function App() {
  return (
    <KizakiProvider client={client}>
      <Dashboard />
    </KizakiProvider>
  );
}

Useful when you want explicit client configuration or a mock client for testing.

useQuery

useQuery(builder, options?) subscribes to a query and returns its current state. Live by default — data updates automatically via WebSocket.

import { useQuery } from "@kizaki/react";
import { select } from "@kizaki/sdk/browser";
import { Project } from "@kizaki/schema";

const projectsQuery = select(Project)
  .fields(Project.id, Project.name)
  .orderBy(Project.createdAt, "desc");

function ProjectList() {
  const { data, totalCount, status, error, refetch } = useQuery(projectsQuery);

  if (status === "loading") return <p>Loading projects...</p>;
  if (status === "error") return <p>{error.message}</p>;

  return (
    <ul>
      {data.map((project) => (
        <li key={project.id}>{project.name}</li>
      ))}
    </ul>
  );
}

Return Value

FieldTypeDescription
dataRow[] | undefinedQuery results, typed to match selected fields
totalCountnumber | undefinedTotal matching rows (before limit/offset)
status"loading" | "success" | "error"Current query state
errorError | undefinedError object if the query failed
refetch() => voidManually trigger a refetch

Options

OptionTypeDefaultDescription
livebooleantrueSubscribe to live updates. Set false for a one-shot fetch
// One-shot fetch — no live updates
const { data, status } = useQuery(projectsQuery, { live: false });

useMutation

useMutation(fn, options?) wraps an async function (typically from @kizaki/client) with loading state, error handling, and cache invalidation.

import { useMutation, useQuery } from "@kizaki/react";
import { select } from "@kizaki/sdk/browser";
import { createProject } from "@kizaki/client";
import { Project } from "@kizaki/schema";

const projectsQuery = select(Project).orderBy(Project.createdAt, "desc");

function CreateProjectButton() {
  const create = useMutation(createProject, {
    invalidate: [projectsQuery],
  });

  return (
    <>
      <button onClick={() => create.mutate("New Project")} disabled={create.pending}>
        New Project
      </button>
      {create.error && <p>{create.error.toUserMessage()}</p>}
    </>
  );
}

Return Value

FieldTypeDescription
mutate(...args) => PromiseCall the wrapped function with its original arguments
pendingbooleanTrue while the mutation is in flight
errorError | undefinedError from the most recent failed call

Options

OptionTypeDescription
invalidateBuilder[] | "all"Query builders to refetch after success. "all" invalidates every active query
optimistic(client, ...args) => rollbackOptimistic update function (see below)
onSuccess(result) => voidCallback after successful mutation
onError(error) => voidCallback after failed mutation

useSubscription

useSubscription(builder) is always-live. Returns an explicit unsubscribe function for manual lifecycle control.

import { useSubscription } from "@kizaki/react";
import { select } from "@kizaki/sdk/browser";
import { Message } from "@kizaki/schema";

function ChatFeed({ channelId }: { channelId: string }) {
  const { data, status, unsubscribe } = useSubscription(
    select(Message)
      .where(eq(Message.channelId, channelId))
      .orderBy(Message.createdAt, "desc")
      .limit(100)
  );

  if (status === "loading") return <p>Connecting...</p>;
  return (
    <ul>
      {data.map((msg) => (
        <li key={msg.id}>{msg.text}</li>
      ))}
    </ul>
  );
}

For most cases, useQuery with the default live: true is sufficient. Use useSubscription when you need explicit unsubscribe control.

usePresence

usePresence(channel, id?) tracks connected user counts. Updates automatically as users join and leave.

import { usePresence } from "@kizaki/react";

function ViewerCount({ docId }: { docId: string }) {
  const { count } = usePresence("document", docId);

  return <span>{count} {count === 1 ? "person" : "people"} viewing</span>;
}

useBroadcast

useBroadcast(channel, id?) provides ephemeral messaging for real-time collaboration. Messages are not persisted.

import { useBroadcast } from "@kizaki/react";

function CollaborativeCursor({ docId }: { docId: string }) {
  const { send, lastMessage } = useBroadcast("document", docId);

  const handleMouseMove = (e: React.MouseEvent) => {
    send({ type: "cursor", x: e.clientX, y: e.clientY });
  };

  return (
    <div onMouseMove={handleMouseMove}>
      {lastMessage?.type === "cursor" && (
        <div
          style={{ position: "absolute", left: lastMessage.x, top: lastMessage.y }}
          className="remote-cursor"
        />
      )}
    </div>
  );
}

lastMessage is undefined until the first message arrives.

Optimistic Updates

The optimistic option on useMutation updates the UI before server confirmation. If the mutation fails, the rollback function restores previous state.

import { useMutation, useQuery } from "@kizaki/react";
import { select } from "@kizaki/sdk/browser";
import { createProject } from "@kizaki/client";
import { Project } from "@kizaki/schema";

const projectsQuery = select(Project).orderBy(Project.createdAt, "desc");

function CreateProjectButton() {
  const create = useMutation(createProject, {
    invalidate: [projectsQuery],
    optimistic: (client, name) => {
      const rollback = client.updateQuery(projectsQuery, (current) => ({
        ...current,
        rows: [
          { id: "temp", name, createdAt: new Date() },
          ...current.rows,
        ],
      }));
      return rollback;
    },
  });

  return (
    <button onClick={() => create.mutate("New Project")} disabled={create.pending}>
      New Project
    </button>
  );
}

The optimistic function receives the BrowserKizakiClient and the mutation arguments. Call client.updateQuery() to patch cached state and return the rollback function. On success, the invalidate list triggers a real refetch. On failure, the rollback restores the previous cache state.

Error Handling

Both useQuery and useMutation expose errors through their return values.

const { data, error, status } = useQuery(projectsQuery);

if (status === "error") {
  return <p>{error.message}</p>;
}

For mutations:

const create = useMutation(createProject, { invalidate: [projectsQuery] });

// In your JSX:
{create.error && <p>{create.error.toUserMessage()}</p>}

SDK errors carry typed code fields for programmatic handling. Use .toUserMessage() for display text.

Auth Hooks

Authentication hooks are available from @kizaki/sdk/auth/react:

  • useAuth() — manages auth state, exposes user, loading, login, signup, logout, and OAuth helpers
  • AuthGate — renders children when authenticated, a fallback when not

See the Auth reference.

Related guide: Realtime

On this page