@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
- Preload and navigate — multiple queries, server-only extras, readiness, and cancellation.
- Server rendering — seed lifetime, read failures, and private data.
- The browser client — mutations and client cleanup.