React Adapter
Routier supports two React data-loading styles:
| Choose | Use 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.
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.
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.
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
useQueryfor 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
- React Hooks — complete
useQueryAPI, dependencies, cleanup, and Suspense example. - React Best Practices — datastore lifetime and subscription patterns.
- Reactive and Suspense Pagination — page-number and page-size examples using both approaches.