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.
The Transport Bridge
Section titled “The Transport Bridge”τ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";Why This Approach?
Section titled “Why This Approach?”Libraries like TanStack Query, SWR, and Apollo already solve data orchestration brilliantly. τjs’ focus is on server-side rendering and route-level data contracts.
Basic Usage
Section titled “Basic Usage”Direct Fetch
Section titled “Direct Fetch”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 } }}TanStack Query Integration
Section titled “TanStack Query Integration”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} /> );}Fetching Different Routes
Section titled “Fetching Different Routes”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} />;}Error Handling
Section titled “Error Handling”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; } }}How It Works
Section titled “How It Works”When you call fetchRouteData('/dashboard'):
- Client sends
GET /__taujs/data?url=/dashboard - Server matches the route from your
taujs.config.ts - Server runs the same
attr.datahandler with the same security context - Server returns
{ data: {...} } - 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-pageSecurity
Section titled “Security”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.datahandler receives the samectx.userobject - Service calls respect the same authentication context
- No way to bypass route-level auth
Error Handling ✅
- Throws
AppErrorwith structured error info - Returns proper HTTP status codes
- Logs failures with the same tracing infrastructure
Advanced Patterns
Section titled “Advanced Patterns”Polling with TanStack Query
Section titled “Polling with TanStack Query”const { data } = useQuery({ queryKey: ["dashboard"], queryFn: () => fetchRouteData("/dashboard"), refetchInterval: 5000, // Poll every 5 seconds});Prefetching on Hover
Section titled “Prefetching on Hover”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> );}Optimistic Updates
Section titled “Optimistic Updates”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> );}