Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Queries in React

Ankurah’s Model derive generates TypeScript classes for queries, views, and mutation handles. React observation lives in the separate @ankurah/react-hooks package, which you bind to the ReactObserver exported by your WASM crate.

Setup

Create one app-local hooks module, src/ankurah-hooks.ts:

import React from "react";
import { createAnkurahReactHooks } from "@ankurah/react-hooks";
import { ReactObserver } from "your-wasm-bindings";

export const { useObserve, signalObserver } =
  createAnkurahReactHooks({ React, ReactObserver });

Initialize the WASM module and your app’s Ankurah client before rendering a component that calls ctx(). The maintained React template exposes that second readiness step as ready():

import { createRoot } from "react-dom/client";
import initBindings, { ready } from "your-wasm-bindings";
import App from "./App";

async function main() {
  await initBindings();
  await ready();
  createRoot(document.getElementById("root")!).render(<App />);
}

void main();

If your binding crate exposes a differently named initializer, await that instead; ready() is application wiring from the template, not a method generated by Model.

Creating Queries

The examples below use React’s lifecycle hooks plus types generated by the Ankurah model bindings:

import { useEffect, useMemo, useState } from "react";
import {
  Album,
  ctx,
  AlbumLiveQuery,
  AlbumView,
} from "ankurah-org-example-wasm-bindings";

Import the wrapper from the app-local hooks module created in Setup:

import { signalObserver } from "./ankurah-hooks";

Use the static .query() method on any model class:

let albumsQuery: AlbumLiveQuery | undefined;

function queryAlbums(): AlbumLiveQuery {
  albumsQuery ??= Album.query(ctx(), "year > 1985");
  return albumsQuery;
}

The query returns immediately with a LiveQuery object. Results stream in as they become available. This example caches the query because its Ankurah client is also a singleton; that avoids duplicate registration if React development StrictMode probes a component twice.

Signal Observation

Ankurah uses signals for reactivity. To make a React component reactive, wrap it with signalObserver:

interface Props {
  albums: AlbumLiveQuery;
}
/* Bind a React observer to the component. */
const AlbumList = signalObserver(({ albums }: Props) => {
  return (
    <ul>
      {/* Reading items registers this render as a live-query observer. */}
      {albums.items.map((album) => (
        <li key={album.id.to_base64()}>{album.name}</li>
      ))}
    </ul>
  );
});

The signalObserver wrapper:

  1. Creates a reactive observer for the component render
  2. Automatically tracks which signals are accessed during render
  3. Re-renders the component when those signals change

How It Works

When you access albums.items inside a component wrapped with signalObserver, the observer tracks this access. When the live query’s results change—whether from local changes or remote sync—the component automatically re-renders.

Creating Entities

Use a transaction to create new entities:

export async function createAlbum(
  name: string,
  artist: string,
  year: number,
): Promise<AlbumView> {
  const transaction = ctx().begin();
  const album = await Album.create(transaction, { name, artist, year });
  await transaction.commit();
  return album;
}

Updating Entities

Views are read-only. Call .edit(transaction) to obtain the generated mutable handle, mutate its active field wrapper, then commit:

export async function renameAlbum(
  album: AlbumView,
  name: string,
): Promise<void> {
  const transaction = ctx().begin();
  album.edit(transaction).name.replace(name);
  await transaction.commit();
}

String fields use the Yrs text backend by default, hence .replace(...). LWW-backed fields expose .set(...) instead.

Querying All Entities

Use an empty selection string to match all entities. Give this query the same explicit lifetime as a filtered query:

const allAlbums = Album.query(ctx(), "");

Query Lifetime

Creating a LiveQuery registers a live resource; do not recreate one on every render. For a fixed, app-lifetime query, use a cached factory such as queryAlbums() above. The example then calls that factory from useMemo in the ready-only component.

For a component-owned dynamic query, construct it in an effect and explicitly free the previous WASM handle during cleanup:

export function useAlbumsByArtist(artist: string): AlbumLiveQuery | null {
  const [albums, setAlbums] = useState<AlbumLiveQuery | null>(null);

  useEffect(() => {
    const query = Album.query(ctx(), "artist = ?", artist);
    setAlbums(query);
    return () => query.free();
  }, [artist]);

  return albums;
}

Use ? placeholders and variadic substitution values for dynamic input. Do not interpolate user-controlled strings into AnkQL source.

Reactive State with JsValueMut

For local reactive state that integrates with the signal system:

import { useMemo } from "react";
import { AlbumView, JsValueMut } from "your-wasm-bindings";

function useSelectedAlbum() {
  const [selectedAlbum, selectedAlbumRead] = useMemo(
    () => JsValueMut.newPair<AlbumView | null>(null),
    [],
  );

  // Reading inside signalObserver tracks this value.
  const album = selectedAlbumRead.get();

  const selectAlbum = (next: AlbumView | null) => selectedAlbum.set(next);
  return { album, selectAlbum };
}

Next Steps