Skip to content
Guides contents

GuidesUI & local state

TanStack Start

Connect Rindle to TanStack Start with a provider, route loaders, and optional server preloads.

View as Markdown

@rindle/tanstack connects named Rindle queries to TanStack Router and TanStack Start. It supplies two objects: a route loader and a React Provider. The loader prepares route data. The provider moves the initial server-rendered results into the live browser client.

This guide continues the public issue-list example in Server rendering. That page defines the SQL table, named query, API factory, preloader, browser boot function, and list component. This page supplies their TanStack integration.

For a new application with this wiring already installed, use create-rindle. The generated chat example has different tables and queries, but uses the same adapter.

Before you start

Use an existing TanStack Start project with its Vite plugin, router, and generated route tree. Install the adapter:

pnpm add @rindle/tanstack

The following modules come from the SSR example:

Module Exports Purpose
shared/schema.gen.ts schema Generated table types
shared/queries.ts recentIssuesQuery The named query shared by both tiers
server/ssr-api.ts createReadApi Public query authority and trusted data-tier connection
server/preload.ts preloadQueries One-shot server reads converted to a seed
src/rindle-client.ts bootClient Memoized browser-only client startup
src/IssueList.tsx IssueList The component that reads the live query

All six modules are defined in the SSR guide. They are application files, not exports from @rindle/tanstack.

Create the integration

Create one adapter that shares the browser client between route loaders and the provider:

// src/rindle-tanstack.ts
import { createRindleTanStack } from "@rindle/tanstack";
import { schema } from "../shared/schema.gen.ts";
import { bootClient } from "./rindle-client.ts";

export const rindle = createRindleTanStack({
  schema,
  boot: bootClient,
  preload: async (queries) => {
    if (!import.meta.env.SSR) return {};
    const { preloadQueries } = await import("../server/preload.ts");
    return preloadQueries(queries);
  },
});

The adapter calls preload only from its server branch. Vite’s static SSR guard also keeps the authority module and its credentials out of the browser bundle. The adapter calls boot only in the browser and memoizes its promise.

Mount the root provider

In the root document, put rindle.Provider around the route outlet. The following is a complete minimal root route. Preserve any existing head configuration or layout from your application:

// src/routes/__root.tsx
import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router";
import { rindle } from "../rindle-tanstack.ts";

export const Route = createRootRoute({
  shellComponent: RootDocument,
});

function RootDocument() {
  return (
    <html lang="en">
      <head><HeadContent /></head>
      <body>
        <rindle.Provider>
          <Outlet />
        </rindle.Provider>
        <Scripts />
      </body>
    </html>
  );
}

The provider combines the rindle loader-data fields from all matched routes. If two routes supply the same seed key, the later matched route wins. It passes that combined state to RindleSSR, so you do not also mount SsrApp from the framework-neutral SSR example.

Declare the route’s query

The home route prepares the same query that IssueList reads:

// src/routes/index.tsx
import { createFileRoute } from "@tanstack/react-router";
import { recentIssuesQuery } from "../../shared/queries.ts";
import { IssueList } from "../IssueList.tsx";
import { rindle } from "../rindle-tanstack.ts";

export const Route = createFileRoute("/")({
  loader: rindle.loader({
    query: () => recentIssuesQuery(),
  }),
  component: IssueList,
});

On the server, the loader preloads the query and returns { rindle: seed }. On browser navigation, it calls the shared client’s ensure method and returns an empty seed. Live subscriptions own browser data after hydration.

The default readiness policy is until: "present": existing local rows can let navigation finish before the server confirms the query. Use until: "complete" when the route must wait for its server result. A confirmed empty result satisfies both policies. See Preload and navigate.

Expose the public query endpoint

The SSR preloader calls the authority in-process. The browser still needs an HTTP endpoint to request its live-query lease. Add this route:

// src/routes/api.rindle.query.tsx
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/api/rindle/query")({
  server: {
    handlers: {
      POST: async ({ request }) => {
        try {
          const { createReadApi } = await import("../../server/ssr-api.ts");
          const body = await request.json();
          const api = createReadApi();
          try {
            const result = await api.handleQueryJson(body, {
              user: undefined, // Matches the public SSR read.
              request,
            });
            return Response.json(result);
          } finally {
            api.close();
          }
        } catch (error) {
          const { RindleApiError } = await import("@rindle/api-server");
          const status = error instanceof RindleApiError ? error.status
            : error instanceof SyntaxError ? 400 : 500;
          console.error(error);
          return Response.json({ error: "Query request failed" }, { status });
        }
      },
    },
  },
});

Keep the server-only imports inside the handler. TanStack Start removes the handler from the browser route module. If TypeScript does not recognize the server route option, include this declaration in your project:

// src/tanstack-start.d.ts
import type {} from "@tanstack/react-start";

Start the application with the rindle dev command from the SSR guide. Open / to read the seeded list. A SQL change to issue then updates the mounted list through its live subscription.

This read-only example needs no mutation route. An app with optimistic writes also mounts handleMutateJson and configures its mutator registry. An HTTP one-shot read endpoint is optional when SSR calls handleReadJson in-process.

Identity and routing context

A loader’s query factory runs before the adapter awaits browser startup. If query construction depends on a user identity, resolve that identity before the factory runs. Do not rely on bootClient to initialize it later.

The preload callback receives (queries, loaderContext). The adapter forwards that context without authenticating it or extracting a request. For private data, your framework integration must provide the verified request context to the server preloader.

The default RindleLoaderContext describes params, deps, context, location, abortController, preload, and cause. Applications can supply a compatible, more specific context type to rindle.loader<YourContext>(...).

Next steps