Skip to content

React Adapter

Routier supports two React data-loading styles:

ChooseUse when
useQuery with .subscribe()The component should update whenever matching stored data changes
React 19 use() with <Suspense>Each request is a one-time Promise read and loading belongs to a Suspense boundary

⚛️ See React Integration Live

Explore a working React example with useQuery, live queries, and reactive updates.

Open Live Playground →|View and Edit Code →

Live query with useQuery

useQuery connects a callback terminal to React state. Adding .subscribe() delivers the initial result and reruns the query when matching stored data changes. Return the query chain so the hook receives its unsubscribe function.

tsx
import { useQuery } from "@routier/react";
import { useDataStore } from "./DexieStore"; // Your app's datastore hook/context

export function ProductsList() {
  const dataStore = useDataStore();

  const products = useQuery(
    callback => dataStore.products.subscribe().toArray(callback),
    [dataStore],
  );

  if (products.status === "pending") return <div>Loading…</div>;
  if (products.status === "error") return <div>Error loading products</div>;

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

This style exposes explicit pending, error, and success states. Dependencies behave like useEffect dependencies and recreate the subscription when they change.

Suspense read (React 19)

React 19's use() reads the Promise returned by a Routier async terminal such as toArrayAsync(). The nearest <Suspense> boundary displays its fallback while that Promise is pending.

tsx
import { Suspense, use, useRef } from "react";
import { useDataStore } from "./DexieStore"; // Your app's datastore hook/context

type AppDataStore = ReturnType<typeof useDataStore>;
type ProductsPromise = ReturnType<AppDataStore["products"]["toArrayAsync"]>;

function ProductsListContent({ request }: { request: ProductsPromise }) {
  const products = use(request);

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

export function SuspenseProductsList() {
  const store = useDataStore(); // Keep this instance stable (Context or useMemo).
  const requestRef = useRef<ProductsPromise | null>(null);

  // This component does not suspend, so the ref survives while the boundary
  // renders its fallback and retries ProductsListContent.
  requestRef.current ??= store.products.toArrayAsync();

  return (
    <Suspense fallback={<div>Loading products…</div>}>
      <ProductsListContent request={requestRef.current} />
    </Suspense>
  );
}

The Promise must remain stable while React retries the suspended render. This example keeps it in a parent useRef. The parent does not suspend—it commits with the boundary's fallback—so the ref survives retries of ProductsListContent. Do not create the Promise or initialize the ref inside the child that calls use().

This is a one-time read, not a subscription. Later database mutations do not replace the cached result automatically. Use useQuery with .subscribe() when the component must stay live. A rejected Promise is handled by the nearest React error boundary, not the Suspense fallback.

Which should I use?

  • Use useQuery for lists, counters, search results, and detail views that should react to local writes, synchronization, or changes from another tab.
  • Use Suspense for route-level or boundary-level one-time loading where your application already manages stable request Promises and invalidation.
  • Use both in one application when different screens have different lifecycle requirements.

Continue

Released under the MIT License.