Connect the server loader, request-scoped QueryClient, metadata, and HydrationBoundary so direct requests and client navigation render the same plugin content.

A plugin page that works after clicking a link can still render incomplete HTML on a direct visit. In a React Router application, check the server loader and the query data handed to the browser before changing the page component.
This guide covers the React Router framework integration in BTST 3.0.2. It assumes server rendering is enabled and the generated BTST catch-all route, layout, and client factories are in place. A standalone BrowserRouter application needs a different server-rendering setup.
The released createReactRouterPage helper resolves the plugin route, runs its loader, dehydrates the React Query cache, and computes route metadata. Its page component uses HydrationBoundary to restore that cache around the resolved plugin component.
That sequence connects four responsibilities:
| Part | Responsibility |
|---|---|
| React Router server loader | Receives the request and chooses the request-aware stack |
| BTST route loader | Fetches the plugin data needed by that route |
| Dehydrated QueryClient state | Carries eligible cached results into the rendered response |
| HydrationBoundary and provider stack | Make those results available to the browser's plugin hooks |
BTST's released page helper defines this ordering. React Router's route module reference explains the surrounding framework exports.
The BTST 0.2.1 generator uses this route wiring. The local imports refer to the factories generated for your application; this is not a complete route without those files:
import { createReactRouterPage } from "@btst/stack/react-router";
import { getOrCreateQueryClient } from "~/lib/query-client";
import { getStackClient } from "~/lib/stack-client";
import { getStackClientForRequest } from "~/lib/stack-client.server";
const page = createReactRouterPage({
getStackClient,
getQueryClient: getOrCreateQueryClient,
});
export const loader = page.createLoader((queryClient, { request }) =>
getStackClientForRequest(queryClient, {
headers: request.headers,
requestOrigin: new URL(request.url).origin,
}),
);
export const meta = page.meta;
export const ErrorBoundary = page.ErrorBoundary;
export default page.Component;
Use the same alias convention as your project. Passing request headers allows the server client factory to resolve the current request's identity and API transport as configured by the application. Keep that factory in its server module; browser bundles should not import server credentials or backend initialization.
The QueryClient factory must create a fresh cache for server requests while reusing the intended browser instance. A module-level server QueryClient can retain another request's data. Replacing the generated factory with a single shared cache to eliminate duplicate fetches changes the privacy boundary.
If direct HTML lacks the article or plugin content, check whether the catch-all route exports the server loader and whether its resolved plugin route has the expected loader. Inspect the document response, not only the DOM after JavaScript runs.
If HTML contains the content but the browser refetches unexpectedly, inspect the query keys, hydration state, and provider wiring. Some refetches follow normal stale-time settings; a network request alone is not proof that hydration failed. Verify that the browser is using the same resolved API and site configuration and the same query-key shape as the server.
If metadata is missing, retain the helper's loader-before-metadata ordering and meta export. Calling metadata separately before its data has loaded can produce a different result from the rendered page.
The helper also supports custom dehydration options. Review serialized errors and data before exposing them in HTML. Do not place secrets or unrelated user records in a cache that will be dehydrated into a public response.
Test a direct request to a published page, a refresh, and navigation from another route. Inspect the initial HTML, title and canonical metadata, browser console, and visible content. Then repeat with a missing route and a controlled loader error, checking the actual HTTP status as well as the error UI.
For protected content, test anonymous and authorized requests independently and inspect the serialized response for private data. Hydration is data transfer, not authorization. The React Router permission guide covers loader and API access checks, while the BTST installation reference supplies the complete framework wiring.