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:
- Creates a reactive observer for the component render
- Automatically tracks which signals are accessed during render
- 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
- Reactivity & Signals - The signals model behind these hooks
- Querying Data - Overview of fetch vs query
- Query Syntax - Full AnkQL syntax reference