Skip to content

React Integration

Routier supports live React subscriptions through useQuery and one-time React 19 Suspense reads through Routier's Promise-based terminal methods.

⚛️ Interactive React Demo

See Routier's React integration in action with live examples of `useQuery`, live queries, and reactive updates.

Open CodeSandbox Demo →

Features

  • Live Queries: useQuery re-renders when subscribed data changes
  • Suspense Reads: React 19 use() reads toArrayAsync() and other Promise terminals
  • Type Safe: Discriminated hook state and inferred query result types
  • Lifecycle Safe: Dependency-driven subscription cleanup

Choose a loading style

StyleBehavior
useQuery + .subscribe()Initial result plus live updates; explicit pending/error/success state
React 19 use() + <Suspense>One result per stable Promise; boundary-based loading and errors through an error boundary

Live-query quick start

tsx
import { useQuery } from "@routier/react";
import { useDataStore } from "./hooks/useDataStore";

function ProductsList() {
  const dataStore = useDataStore(); // Must be memoized in useDataStore hook

  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!</div>;

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

Important: Your useDataStore hook must use useMemo or Context to keep the DataStore instance stable. Creating a new DataStore on every render recreates subscriptions continuously. See Best Practices for details.

Suspense quick start (React 19)

Use an async terminal and keep its Promise in a parent ref so the component that calls use() receives the same request on every retry:

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>
  );
}

This performs a one-time read; it does not subscribe to later database changes. See the React Adapter for the complete comparison.

Installation

bash
npm install @routier/react
# Install peer dependencies
npm install react react-dom

Core Concepts

useQuery Hook

The useQuery hook subscribes to Routier collections and returns loading, error, and success states:

tsx
const result = useQuery(
  (callback) => collection.subscribe().toArray(callback),
  [
    /* dependencies */
  ]
);

// result.status: 'pending' | 'success' | 'error'
// result.loading: boolean
// result.error: Error | null
// result.data: T | undefined

Automatic Updates

Queries automatically re-render when your data changes:

tsx
// Add a product
await dataStore.products.addAsync({ name: "New Product" });
await dataStore.saveChangesAsync();

// Component automatically re-renders with new data!

Type Safety

TypeScript knows exactly what state your component is in:

tsx
if (products.status === "success") {
  // TypeScript knows products.data is defined here
  console.log(products.data); // ✅ Safe
}

Concepts You'll Need

Released under the MIT License.