Skip to content

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.

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
taujs.config.ts
{
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();
}
}
}

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

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.

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.

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 headContent runs
  • Requires meta property for reliable SEO

See Head Management for details on meta.

Access server data in your components:

client/App.tsx
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>
);
}

τ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.data calls 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.

  • 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.

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
client/entry-server.tsx
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>`;
},
});