Skip to content

Client Data Fetching

After SSR delivers your initial page, you often need to refetch data on user interactions, navigate between routes, or poll for updates.

τjs provides a lightweight transport bridge in @taujs/react that connects your client to the server’s /__taujs/data endpoint.

τjs doesn’t ship a full data orchestration layer (caching, deduplication, retries, prefetch). Instead, it provides primitives that work with any data library:

import {
fetchRouteData, // Fetch data for any route
readInitialDataOnce, // Read SSR boot data (once) for advanced patterns
getCurrentPath, // Get current pathname + search
RouteDataError, // Structured error class
} from "@taujs/react";

Libraries like TanStack Query, SWR, and Apollo already solve data orchestration brilliantly. τjs’ focus is on server-side rendering and route-level data contracts.

import { fetchRouteData } from "@taujs/react";
async function loadDashboard() {
try {
const data = await fetchRouteData("/dashboard");
console.log(data); // Same shape as your attr.data returned
} catch (err) {
if (err instanceof RouteDataError) {
console.log(err.status); // 404, 403, 500, etc.
console.log(err.code); // Optional error code
}
}
}

For production apps, integrate with TanStack Query using τjs primitives directly:

import { useQuery } from "@tanstack/react-query";
import {
fetchRouteData,
readInitialDataOnce,
getCurrentPath,
RouteDataError,
} from "@taujs/react";
function DashboardPage() {
const currentPath = getCurrentPath() ?? "/dashboard";
const { data, error, isLoading, refetch } = useQuery({
queryKey: ["route-data", currentPath],
queryFn: () => fetchRouteData(currentPath),
initialData: () => {
// Read SSR data on first render only
const bootData = readInitialDataOnce();
return bootData ?? undefined;
},
});
if (isLoading) return <Spinner />;
if (error instanceof RouteDataError) {
if (error.status === 404) return <NotFound />;
if (error.status === 403) return <Forbidden />;
return <ErrorView error={error} />;
}
return (
<Dashboard user={data.user} metrics={data.metrics} onRefresh={refetch} />
);
}
function UserProfile({ userId }: { userId: string }) {
const { data, error, isLoading } = useQuery({
queryKey: ["user", userId],
queryFn: () => fetchRouteData(`/users/${userId}`),
});
if (isLoading) return <Spinner />;
if (error) return <ErrorView error={error} />;
return <ProfileCard user={data.user} />;
}
import { RouteDataError } from "@taujs/react";
try {
const data = await fetchRouteData("/dashboard");
} catch (err) {
if (err instanceof RouteDataError) {
console.log(err.status); // HTTP status code
console.log(err.code); // Optional error code from server
console.log(err.statusText); // "Not Found", "Forbidden", etc.
console.log(err.body); // Full response body
switch (err.status) {
case 404:
// Route or data handler not found
break;
case 403:
// Authentication failed
break;
case 500:
// Server error
break;
}
}
}

When you call fetchRouteData('/dashboard'):

  1. Client sends GET /__taujs/data?url=/dashboard
  2. Server matches the route from your taujs.config.ts
  3. Server runs the same attr.data handler with the same security context
  4. Server returns { data: {...} }
  5. Client receives the exact shape your route defined

Important: Routes Must Be Defined in Config

Section titled “Important: Routes Must Be Defined in Config”

/__taujs/data only works for routes defined in taujs.config.ts. If you have client-only routes (no SSR), don’t add them to the config.

For client-only routes, use standard API endpoints instead:

// ❌ Don't do this for client-only routes
{
path: '/client-only-page',
attr: { data: async () => ({ ... }), render: 'ssr' }
}
// ✅ Do this instead
// 1. No route in taujs.config.ts
// 2. Client router handles /client-only-page
// 3. Component fetches from /api/client-only-page

The /__taujs/data endpoint respects your route configuration:

Authentication

  • If your route has attr.middleware.auth, the endpoint requires authentication
  • Uses the same fastify.authenticate() decorator
  • Returns 401/403 if auth fails

Authorisation

  • Your attr.data handler receives the same ctx.user object
  • Service calls respect the same authentication context
  • No way to bypass route-level auth

Error Handling

  • Throws AppError with structured error info
  • Returns proper HTTP status codes
  • Logs failures with the same tracing infrastructure
const { data } = useQuery({
queryKey: ["dashboard"],
queryFn: () => fetchRouteData("/dashboard"),
refetchInterval: 5000, // Poll every 5 seconds
});
import { useQueryClient } from '@tanstack/react-query';
import { fetchRouteData } from '@taujs/react';
function NavLink({ to }: { to: string }) {
const queryClient = useQueryClient();
return (
href={to}
onMouseEnter={() => {
queryClient.prefetchQuery({
queryKey: ['route-data', to],
queryFn: () => fetchRouteData(to)
});
}}
>
Dashboard
</a>
);
}
import { useQueryClient } from "@tanstack/react-query";
import { fetchRouteData, getCurrentPath } from "@taujs/react";
function Counter() {
const queryClient = useQueryClient();
const currentPath = getCurrentPath();
const { data, refetch } = useQuery({
queryKey: ["route-data", currentPath],
queryFn: () => fetchRouteData(currentPath || "/"),
});
async function increment() {
// Optimistic update
queryClient.setQueryData(["route-data", currentPath], (old: any) => ({
...old,
count: old.count + 1,
}));
// Send to server
await fetch("/api/increment", { method: "POST" });
// Refetch to confirm
refetch();
}
return (
<div>
<p>Count: {data.count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}