Data Loading
τjs provides a route-first, declarative way to load data for SSR and streaming. This page builds on the request contract model / data ownership described earlier, but can be read independently.
Overview
Section titled “Overview”Data loading happens at the route level through the attr.data function. This function:
- Runs on the server
- Receives route parameters and request context
- Returns data that’s injected into your page
Basic Data Loading
Section titled “Basic Data Loading”{ path: '/users/:id', attr: { render: 'ssr', data: async (params, ctx) => { const res = await fetch(`https://api.example.com/users/${params.id}`); return await res.json(); } }}Request Context
Section titled “Request Context”Your data handler receives a context object:
type RequestContext = { traceId: string; // Request trace ID logger: Logger; // Scoped logger headers: Record<string, string>; // Request headers};Example:
data: async (params, ctx) => { ctx.logger.info({ userId: params.id }, "Loading user");
const res = await fetch(`/api/users/${params.id}`, { headers: { "x-trace-id": ctx.traceId, authorization: ctx.headers.authorization || "", }, });
return await res.json();};Service Descriptors
Section titled “Service Descriptors”Delegate to registered services for better separation:
{ path: '/users/:id', attr: { render: 'ssr', data: async (params) => ({ serviceName: 'UserService', serviceMethod: 'getUser', args: { id: params.id } }) }}τjs calls the service automatically and returns the result. See the Services section for further information.
Rendering Modes
Section titled “Rendering Modes”SSR (Server-Side Rendering)
Section titled “SSR (Server-Side Rendering)”Data loads completely before rendering:
{ path: '/products', attr: { render: 'ssr', data: async () => { const products = await db.products.findAll(); return { products }; } }}Characteristics:
- Data fully available in
headContent - Complete HTML in single response
- Best for SEO-critical pages
Performance tip: For content that doesn’t change per-user (marketing pages, documentation), you can combine SSR with edge caching to serve essentially static pages. See Edge-Cached Static Pages.
Streaming SSR
Section titled “Streaming SSR”Shell sent immediately, data may load progressively:
{ path: '/dashboard', attr: { render: 'streaming', meta: { // Required for streaming title: 'Dashboard', description: 'User dashboard' }, data: async () => { const metrics = await fetchMetrics(); return { metrics }; } }}Characteristics:
- Faster Time to First Byte
- Data may not be ready when
headContentruns - Requires
metaproperty for reliable SEO
See Head Management for details on meta.
Using Data on the Client
Section titled “Using Data on the Client”SSR Store
Section titled “SSR Store”Access server data in your components:
import { useSSRStore } from "@taujs/react";
export function App() { const data = useSSRStore<{ products: Product[] }>();
return ( <div> {data.products.map((product) => ( <ProductCard key={product.id} product={product} /> ))} </div> );}Client-side updates after SSR
Section titled “Client-side updates after SSR”τjs only defines how data is loaded for the initial render (attr.data) and how that data is made available to your renderer (for example via useSSRStore()).
For updates after hydration (refreshing a dashboard, polling, user-triggered reloads), use standard client-side data fetching patterns (TanStack Query, SWR, custom hooks) against explicit API or service endpoints.
A common pattern is to reuse the same underlying “use case” or service logic in both places:
attr.datacalls into domain/service code for the initial render- an
/api/*endpoint calls the same domain/service code for client refresh - the client fetches from
/api/*using your preferred data library
This keeps SSR orchestration separate from client API concerns and avoids implicitly exposing server route logic to the browser.
Advanced: RouteContext
Section titled “Advanced: RouteContext”attr.data(params, ctx)enable you to fetch data.- Components read that data via
useSSRStore<YourType>().
RouteContext exists for a narrower job: making your renderer and <head> logic route-aware without the need of a client-side router.
What RouteContext gives you
Section titled “What RouteContext gives you”Each request gets a routeContext object built from your taujs.config.ts, including things like:
appId- matched route definition (
path,attr, etc.) params- resolved data key (for debugging / telemetry)
You can thread that into @taujs/react so your renderer can do things like:
- tweak
<title>/ meta based on the matched route - change logging / telemetry behaviour per route
- handle “families” of routes (e.g. all
/admin/*) without bolting that logic into your components - Zero client-side routing but still wanting route-aware rendering
Wiring RouteContext into the renderer
Section titled “Wiring RouteContext into the renderer”import { createRenderer } from "@taujs/react";
import { AppBootstrap } from "./AppBootstrap";import config from "../taujs.config";
import type { RouteContext } from "@taujs/server";
export const { renderSSR, renderStream } = createRenderer< Record<string, unknown>, RouteContext<typeof config>>({ appComponent: ({ location, routeContext }) => ( <AppBootstrap location={location} routeContext={routeContext} /> ), headContent: ({ data, meta, routeContext }) => { const anyData = data as { title?: string; description?: string };
const baseTitle = anyData.title ?? (meta.title as string | undefined) ?? "My App";
const section = routeContext?.path.startsWith("/admin") ? " · Admin" : routeContext?.path.startsWith("/docs") ? " · Docs" : "";
return `<title>${baseTitle}${section}</title>`; },});