React
@taujs/react
Section titled “@taujs/react”React SSR (Server-Side Rendering) with Streaming Support
A lightweight, production-ready React SSR library with streaming capabilities, built for modern TypeScript applications. Designed as part of the taujs (τjs) ecosystem but fully standalone and runtime-agnostic.
τjs @taujs/react
Table of Contents
Section titled “Table of Contents”- Overview
- Installation
- Core Concepts
- API Reference
- Usage in taujs Ecosystem
- Standalone Usage
- Advanced Examples
- TypeScript Support
- Best Practices
Overview
Section titled “Overview”@taujs/react provides a complete SSR solution for React applications with:
- Streaming SSR with React 18+
renderToPipeableStream - Static SSR with
renderToStringfor simpler use cases - Automatic hydration with built-in data synchronisation
- Type-safe data stores using React hooks
- Robust error handling for production environments
- Flexible logging for debugging and monitoring
- Runtime agnostic - works with Node.js, Fastify, Express, or any HTTP server
Installation
Section titled “Installation”npm install @taujs/react react react-domCore Concepts
Section titled “Core Concepts”1. SSR Data Store
Section titled “1. SSR Data Store”The SSRStore is a type-safe data container that synchronises data between server and client:
type SSRStore<T> = { getSnapshot: () => T; getServerSnapshot: () => T; setData: (newData: T) => void; subscribe: (callback: () => void) => () => void; status: "pending" | "success" | "error"; lastError?: Error;};2. Renderer Pattern
Section titled “2. Renderer Pattern”The createRenderer function creates a reusable renderer with your app component and configuration:
const { renderSSR, renderStream } = createRenderer({ appComponent: ({ location }) => <App location={location} />, headContent: ({ data, meta }) => `<title>${data.title}</title>`, streamOptions: { shellTimeoutMs: 10000 },});3. Hydration Strategy
Section titled “3. Hydration Strategy”Client-side hydration automatically:
- Detects SSR data from
window.__INITIAL_DATA__ - Creates a synchronised store
- Hydrates the React tree
- Falls back to CSR if no SSR data exists
API Reference
Section titled “API Reference”Server-Side Rendering
Section titled “Server-Side Rendering”createRenderer<T>(options)
Section titled “createRenderer<T>(options)”Creates a renderer instance with streaming and static SSR capabilities.
Type Signature:
function createRenderer<T extends Record<string, unknown>>(options: { appComponent: (props: { location: string }) => React.ReactElement; headContent: (ctx: { data: T; meta: Record<string, unknown> }) => string; enableDebug?: boolean; logger?: LoggerLike; streamOptions?: StreamOptions;}): { renderSSR: ( initialData: T, location: string, meta?: Record<string, unknown>, signal?: AbortSignal, opts?: { logger?: LoggerLike } ) => Promise<SSRResult>;
renderStream: ( writable: Writable, callbacks: RenderCallbacks<T>, initialData: T | Promise<T> | (() => Promise<T>), location: string, bootstrapModules?: string, meta?: Record<string, unknown>, cspNonce?: string, signal?: AbortSignal, opts?: StreamCallOptions ) => { abort: () => void; done: Promise<void> };};Parameters:
appComponent: Function that returns your root React component, receives{ location: string }headContent: Function that generates HTML head content, receives{ data: T, meta: Record<string, unknown> }. Note: In streaming mode,datamay not be resolved when shell is ready - guard optional fields or rely onmeta.enableDebug: Enable detailed logging (default:false)logger: Custom logger implementationstreamOptions: Streaming configurationshellTimeoutMs: Timeout for initial shell render (default:10000)useCork: Enable write batching with cork/uncork (default:true)
Returns:
Object with two rendering methods:
renderSSR: Static rendering (returns complete HTML)renderStream: Streaming rendering (pipes to writable stream)
renderSSR()
Section titled “renderSSR()”Renders the complete application to a string (static SSR).
Example:
const { renderSSR } = createRenderer({ appComponent: ({ location }) => <App location={location} />, headContent: ({ data }) => `<title>${data.pageTitle}</title>`,});
const result = await renderSSR( { pageTitle: "Home", content: "Welcome!" }, "/home", { requestId: "123" });
// result: { headContent: string, appHtml: string, aborted: boolean }renderStream()
Section titled “renderStream()”Streams the application with React 18+ streaming SSR.
Callbacks:
type RenderCallbacks<T> = { onHead?: (head: string) => boolean | void; // Return false to wait for 'drain' before piping onShellReady?: () => void; onAllReady?: (data: T) => void; onFinish?: (data: T) => void; onError?: (err: unknown) => void;};Backpressure Handling:
- If
onHead()returnsfalse, the renderer will wait for the writable stream’s'drain'event before piping, respecting backpressure. - This is useful when the initial head write causes backpressure on slow connections.
Example:
const { renderStream } = createRenderer({ appComponent: ({ location }) => <App location={location} />, headContent: ({ data }) => `<title>${data.pageTitle}</title>`,});
const { done, abort } = renderStream( res, // Node.js Writable stream (e.g., http.ServerResponse) { onHead: (head) => { console.log("Shell ready, head HTML:", head); }, onShellReady: () => { console.log("Initial HTML ready"); }, onAllReady: (data) => { console.log("All content ready:", data); }, onError: (err) => { console.error("Stream error:", err); }, }, { pageTitle: "Home", content: "Welcome!" }, "/home", "/assets/client.js", // bootstrap module { requestId: "123" }, // meta "nonce-abc123", // CSP nonce abortSignal // AbortSignal for cancellation);
// Wait for completionawait done;Client-Side Hydration
Section titled “Client-Side Hydration”hydrateApp<T>(options)
Section titled “hydrateApp<T>(options)”Hydrates a server-rendered React application on the client.
Type Signature:
function hydrateApp<T>(options: { appComponent: React.ReactElement; rootElementId?: string; enableDebug?: boolean; logger?: LoggerLike; dataKey?: string; onHydrationError?: (err: unknown) => void; onStart?: () => void; onSuccess?: () => void;}): void;Parameters:
appComponent: Your root React component (already instantiated)rootElementId: DOM element ID to hydrate into (default:'root')enableDebug: Enable debug logging (default:false)logger: Custom logger implementationdataKey: Key for initial data on window object (default:'__INITIAL_DATA__')onHydrationError: Callback for hydration errorsonStart: Callback when hydration startsonSuccess: Callback when hydration completes
Example:
// entry-client.tsx (with @taujs/server)import { hydrateApp } from "@taujs/react";import { App } from "./App";
hydrateApp({ appComponent: <App />, rootElementId: "root", enableDebug: import.meta.env.DEV, onHydrationError: (err) => { console.error("Hydration failed:", err); }, onSuccess: () => { console.log("App hydrated successfully!"); },});Complete Client Setup (Standalone):
import React from "react";import { hydrateApp, useSSRStore } from "@taujs/react";
// Your app component that consumes SSR datafunction App() { const data = useSSRStore<{ pageTitle: string; content: string }>();
return ( <div> <h1>{data.pageTitle}</h1> <p>{data.content}</p> </div> );}
// Hydrate the apphydrateApp({ appComponent: <App />, rootElementId: "root", dataKey: "__INITIAL_DATA__", enableDebug: process.env.NODE_ENV === "development", onHydrationError: (err) => { console.error("Hydration failed, falling back to CSR:", err); // Optionally report to error tracking service }, onStart: () => { console.log("Starting hydration..."); }, onSuccess: () => { console.log("Hydration complete!"); // Initialise client-side only features initialiseAnalytics(); },});Behavior:
- If
window[dataKey]exists: Performs SSR hydration with the provided data - If
window[dataKey]is missing: Falls back to client-side rendering (CSR) - Automatically handles
DOMContentLoadedtiming - Wraps your app in
React.StrictModeandSSRStoreProvider
HTML Template Requirements:
Your server-rendered HTML must include:
- The root element:
<div id="root"><!-- SSR content here --></div>- Initial data script (injected by server):
<script> window.__INITIAL_DATA__ = { pageTitle: "Home", content: "Welcome" };</script>- Client bundle:
<script type="module" src="/assets/entry-client.js"></script>Client Data Bridge
Section titled “Client Data Bridge”τjs provides framework-agnostic primitives for accessing route data on the client.
fetchRouteData<T>(pathname, init?)
Section titled “fetchRouteData<T>(pathname, init?)”Fetch route data from the τjs data endpoint.
Type Signature:
function fetchRouteData<T extends RouteData = RouteData>( pathname: string, init?: RequestInit): Promise<T>;Example:
const data = await fetchRouteData("/dashboard");Throws: RouteDataError on non-2xx responses.
readInitialDataOnce<T>()
Section titled “readInitialDataOnce<T>()”Read SSR boot data from window.__INITIAL_DATA__ exactly once.
Type Signature:
function readInitialDataOnce<T extends RouteData = RouteData>(): T | null;Returns: Data on first call, null on subsequent calls or if no SSR data exists.
getCurrentPath()
Section titled “getCurrentPath()”Get the current browser path (pathname + search).
Type Signature:
function getCurrentPath(): string | null;Returns: "/app/dashboard?tab=overview" or null on server.
RouteDataError
Section titled “RouteDataError”Error class thrown by fetchRouteData() on failed requests.
Properties:
class RouteDataError extends Error { readonly status: number; // HTTP status code readonly statusText: string; // Status text readonly code?: string; // Optional error code from server readonly body?: unknown; // Full response body}Example:
try { const data = await fetchRouteData("/dashboard");} catch (err) { if (err instanceof RouteDataError && err.status === 404) { // Handle not found }}See: Client-Side Data Fetching Guide for integration patterns and examples.
Data Store
Section titled “Data Store”createSSRStore<T>(initialDataOrPromise)
Section titled “createSSRStore<T>(initialDataOrPromise)”Creates a store for managing SSR data synchronisation.
Type Signature:
function createSSRStore<T>( initialDataOrPromise: T | Promise<T> | (() => Promise<T>)): SSRStore<T>;Parameters:
initialDataOrPromise: Can be:- Synchronous data:
T - Promise:
Promise<T> - Lazy promise factory:
() => Promise<T>
- Synchronous data:
Returns: SSRStore<T> with methods:
getSnapshot(): Get current data (throws if pending)getServerSnapshot(): Get data for server rendering (throws if pending)setData(newData): Update the storesubscribe(callback): Subscribe to changesstatus: Current status ('pending' | 'success' | 'error')lastError: Last error if status is'error'
Example:
import { createSSRStore } from "@taujs/react";
// Synchronous dataconst store1 = createSSRStore({ user: "Alice", posts: [] });
// Promiseconst store2 = createSSRStore(fetch("/api/data").then((r) => r.json()));
// Lazy promiseconst store3 = createSSRStore(async () => { const data = await fetch("/api/data"); return data.json();});useSSRStore<T>()
Section titled “useSSRStore<T>()”React hook to consume data from an SSR store.
Type Signature:
function useSSRStore<T>(): T;Usage:
import { useSSRStore, SSRStoreProvider } from "@taujs/react";
function MyComponent() { const data = useSSRStore<{ user: string; posts: string[] }>();
return ( <div> <h1>Welcome, {data.user}</h1> <ul> {data.posts.map((post) => ( <li key={post}>{post}</li> ))} </ul> </div> );}
// In your app componentfunction App() { return ( <SSRStoreProvider store={store}> <MyComponent /> </SSRStoreProvider> );}Notes:
- Must be used within
SSRStoreProvider - Uses
useSyncExternalStoreanduseDeferredValuefor React 18+ concurrent features - Automatically suspends if data is pending
SSRStoreProvider
Section titled “SSRStoreProvider”Context provider for SSR store access.
Type Signature:
function SSRStoreProvider<T>(props: { store: SSRStore<T>; children: React.ReactNode;}): JSX.Element;Vite Plugin
Section titled “Vite Plugin”pluginReact(options?)
Section titled “pluginReact(options?)”Wrapper around @vitejs/plugin-react for use in taujs ecosystem.
Type Signature:
function pluginReact(opts?: Parameters<typeof react>[0]): PluginOption;Usage:
import { defineConfig } from "vite";import { pluginReact } from "@taujs/react/plugin";
export default defineConfig({ plugins: [pluginReact()],});Usage in taujs Ecosystem
Section titled “Usage in taujs Ecosystem”With @taujs/server (Complete Integration)
Section titled “With @taujs/server (Complete Integration)”The @taujs/react package is designed to integrate seamlessly with @taujs/server, which handles all the SSR rendering, routing, and data fetching automatically. When used together, you don’t need to call the rendering functions directly - @taujs/server handles everything through declarative configuration.
Declarative Configuration (taujs.config.ts)
Section titled “Declarative Configuration (taujs.config.ts)”import { pluginReact } from "@taujs/react/plugin";import { defineConfig } from "@taujs/server/config";
export default defineConfig({ apps: [ { appId: "root", entryPoint: "", // Root app plugins: [pluginReact()], routes: [ { path: "/:id", attr: { // Data fetching for this route data: async (params, ctx) => { const res = await fetch( `http://localhost:5173/api/initial/${params.id}`, { headers: ctx.headers, } ); return await res.json(); }, // Per-route CSP configuration middleware: { csp: { directives: ({ params }) => { const userId = params.id as string; return { "script-src": [ "'self'", `https://user-${userId}.example.com`, ], }; }, }, }, render: "ssr", // Static SSR }, }, { path: "/:id/:another", attr: { // Service registry pattern (alternative to HTTP fetch) data: async (params) => ({ serviceName: "ServiceExample", serviceMethod: "exampleMethod", args: params, }), meta: { title: "τjs [taujs] - streaming", description: "Streaming page description from route meta", }, render: "streaming", // Streaming SSR }, }, ], }, { appId: "mfe", entryPoint: "@admin", // Microfrontend plugins: [pluginReact()], routes: [ { path: "/mfe/:id", attr: { data: async (params, ctx) => { const res = await fetch( `http://localhost:5173/api/initial/${params.id}`, { headers: ctx.headers, } ); return await res.json(); }, render: "ssr", }, }, ], }, ],});Server Setup (Two Patterns)
Section titled “Server Setup (Two Patterns)”Pattern 1: Full Delegation (Recommended)
import path from "node:path";import Fastify from "fastify";import { createServer } from "@taujs/server";import { serviceRegistry } from "./services";import config from "./taujs.config";
const clientRoot = path.resolve(__dirname, "../client");const fastify = Fastify({ logger: false });
// taujs/server handles all SSR, routing, and renderingawait createServer({ fastify, config, serviceRegistry, clientRoot, alias: { "@client": clientRoot },});
// Add your API routesfastify.get("/api/initial/:id?", (request, reply) => { const { id } = request.params;
if (id) { setTimeout(() => { reply.send({ title: `τjs [taujs] - ${id}`, description: `HTTP API call response with - ${id}`, }); }, 1000); } else { reply.send({ data: "HTTP API call response" }); }});
await fastify.listen({ port: 5173, host: "0.0.0.0" });Pattern 2: Explicit Registration
import path from "node:path";import Fastify from "fastify";import fastifyStatic from "@fastify/static";import { createServer } from "@taujs/server";import { serviceRegistry } from "./services";import config from "./taujs.config";
const clientRoot = path.resolve(__dirname, "../client");
const startServer = async () => { try { const fastify = Fastify({ logger: false });
// Optional: Add compression await fastify.register(import("@fastify/compress"), { global: true });
// Register taujs/server with explicit static assets (see Static assets guide) const { net } = await createServer({ fastify, clientRoot, config, serviceRegistry, staticAssets: { plugin: fastifyStatic, }, debug: { all: false, ssr: true }, });
// Add API routes fastify.get("/api/initial/:id?", (request, reply) => { const { id } = request.params;
if (id) { setTimeout(() => { reply.send({ title: `τjs [taujs] - ${id}`, description: `HTTP API call response with - ${id}`, }); }, 1000); } else { reply.send({ data: "HTTP API call response with route meta" }); } });
await fastify.listen( { port: net.port, host: net.host, }, (err, address) => { if (err) { fastify.log.error(err); process.exit(1); } } ); } catch (error) { console.error("Error starting server:", error); process.exit(1); }};
startServer();Service Registry Pattern
Section titled “Service Registry Pattern”Instead of making HTTP calls in your data loaders, use the service registry pattern:
import { defineServiceRegistry, defineService } from "@taujs/server";
const ServiceExample = defineService({ exampleMethod: async (params: { id: string; another: string }, ctx) => { // Business logic here return { title: `Service Response`, data: params, userId: ctx.user?.id, }; },});
export const serviceRegistry = defineServiceRegistry({ ServiceExample,});// taujs.config.ts - Use service instead of HTTP{ path: '/:id/:another', attr: { data: async (params) => ({ serviceName: 'ServiceExample', serviceMethod: 'exampleMethod', args: params, }), render: 'streaming', },}Key Integration Features
Section titled “Key Integration Features”- Automatic Rendering:
@taujs/serveruses@taujs/reactinternally - you don’t callcreateRendererdirectly - Declarative Routes: Define routes with data loading and rendering strategy in config
- CSP Integration: Per-route CSP policies with dynamic directives
- Service Registry: Type-safe service calls instead of HTTP
- Microfrontend Support: Multiple apps with separate entry points
- Development Mode: Automatic HMR and Vite integration
- Static Assets: Automatic handling of static files and manifests
Standalone Usage
Section titled “Standalone Usage”With Express
Section titled “With Express”import express from "express";import { createRenderer } from "@taujs/react";import { App } from "./App";
const app = express();
const { renderStream } = createRenderer({ appComponent: ({ location }) => <App location={location} />, headContent: ({ data }) => `<title>${data.pageTitle}</title>`,});
app.get("*", async (req, res) => { const data = await fetchPageData(req.path);
res.setHeader("Content-Type", "text/html; charset=utf-8");
// Write opening HTML res.write(` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <script>window.__INITIAL_DATA__=${JSON.stringify(data)};</script> </head> <body><div id="root"> `);
const { done } = renderStream( res, { onAllReady: () => { res.write(`</div></body></html>`); res.end(); }, onError: (err) => { console.error("Stream error:", err); res.destroy(); }, }, data, req.path, "/assets/client.js" );
await done;});
app.listen(3000, () => { console.log("Server listening on port 3000");});With Native Node.js HTTP
Section titled “With Native Node.js HTTP”import http from "node:http";import { createRenderer } from "@taujs/react";import { App } from "./App";
const { renderStream } = createRenderer({ appComponent: ({ location }) => <App location={location} />, headContent: ({ data }) => `<title>${data.pageTitle}</title>`,});
const server = http.createServer(async (req, res) => { const data = { pageTitle: "Home", content: "Hello World" };
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.write(` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <script>window.__INITIAL_DATA__=${JSON.stringify(data)};</script> </head> <body><div id="root"> `);
const { done } = renderStream( res, { onAllReady: () => { res.write(`</div></body></html>`); res.end(); }, onError: (err) => { console.error(err); res.destroy(); }, }, data, req.url || "/", "/assets/client.js" );
await done.catch(() => {}); // Error already handled in callback});
server.listen(3000);With Hono
Section titled “With Hono”import { Hono } from "hono";import { createRenderer } from "@taujs/react";import { streamSSR } from "hono/streaming";import { App } from "./App";
const app = new Hono();
const { renderStream } = createRenderer({ appComponent: ({ location }) => <App location={location} />, headContent: ({ data }) => `<title>${data.pageTitle}</title>`,});
app.get("*", async (c) => { const data = { pageTitle: "Home", content: "Hello from Hono" };
return streamSSR(c, async (stream) => { await stream.write(` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <script>window.__INITIAL_DATA__=${JSON.stringify(data)};</script> </head> <body><div id="root"> `);
const { done } = renderStream( stream, { onAllReady: async () => { await stream.write(`</div></body></html>`); }, onError: (err) => { console.error(err); }, }, data, c.req.path, "/assets/client.js" );
await done; });});
export default app;Static SSR (No Streaming)
Section titled “Static SSR (No Streaming)”For simpler use cases or edge runtimes that don’t support streaming:
import { createRenderer } from "@taujs/react";import { App } from "./App";
const { renderSSR } = createRenderer({ appComponent: ({ location }) => <App location={location} />, headContent: ({ data }) => `<title>${data.pageTitle}</title>`,});
export default { async fetch(request: Request): Promise<Response> { const url = new URL(request.url); const data = { pageTitle: "Home", content: "Hello World" };
const { headContent, appHtml } = await renderSSR(data, url.pathname);
const html = ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> ${headContent} <script>window.__INITIAL_DATA__=${JSON.stringify(data)};</script> </head> <body> <div id="root">${appHtml}</div> <script type="module" src="/assets/client.js"></script> </body> </html> `;
return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" }, }); },};Complete Project Examples
Section titled “Complete Project Examples”Full Project Structure (with @taujs/server)
Section titled “Full Project Structure (with @taujs/server)”my-app/├── client/│ ├── entry-client.tsx # Client hydration entry│ ├── entry-server.tsx # Server render entry│ ├── index.html # HTML template│ ├── App.tsx # Root component│ └── components/│ └── HomePage.tsx├── server/│ ├── index.ts # Server bootstrap│ ├── services/│ │ └── index.ts # Service registry│ └── utils/│ └── index.ts├── taujs.config.ts # taujs configuration├── tsconfig.json├── package.json└── vite.config.tsclient/entry-client.tsx:
import { hydrateApp } from "@taujs/react";import { App } from "./App";
hydrateApp({ appComponent: <App />, enableDebug: import.meta.env.DEV,});client/entry-server.tsx:
import { createRenderer } from "@taujs/react";import { App } from "./App";
export const { renderSSR, renderStream } = createRenderer({ appComponent: ({ location }) => <App location={location} />, headContent: ({ data, meta }) => ` <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${data?.title ?? meta.title ?? "My App"}</title> ${ data?.description ? `<meta name="description" content="${data.description}">` : "" } `, enableDebug: process.env.NODE_ENV === "development",});client/index.html:
<!DOCTYPE html><html lang="en"> <head> <!--ssr-head--> <script type="module" src="/@vite/client"></script> </head> <body> <div id="root"><!--ssr-html--></div> <script type="module" src="/entry-client.tsx"></script> </body></html>client/App.tsx:
import { useSSRStore } from "@taujs/react";import { HomePage } from "./components/HomePage";
export function App({ location }: { location?: string }) { const data = useSSRStore<{ title: string; description: string; userId?: string; }>();
return ( <div> <h1>{data.title}</h1> <p>{data.description}</p> {data.userId && <HomePage userId={data.userId} />} </div> );}server/index.ts:
import path from "node:path";import Fastify from "fastify";import { createServer } from "@taujs/server";import { serviceRegistry } from "./services";import config from "../taujs.config";
const clientRoot = path.resolve(__dirname, "../client");const fastify = Fastify({ logger: false });
await createServer({ fastify, config, serviceRegistry, clientRoot, debug: { ssr: true },});
// API routesfastify.get("/api/users/:id", async (request, reply) => { const user = await db.users.findById(request.params.id); return { user };});
await fastify.listen({ port: 5173, host: "0.0.0.0" });server/services/index.ts:
import { defineServiceRegistry, defineService } from "@taujs/server";
const UserService = defineService({ getUser: async (params: { id: string }, ctx) => { const user = await db.users.findById(params.id); return { user, title: `User: ${user.name}`, description: user.bio, }; },});
export const serviceRegistry = defineServiceRegistry({ UserService,});taujs.config.ts:
import { pluginReact } from "@taujs/react/plugin";import { defineConfig } from "@taujs/server/config";
export default defineConfig({ apps: [ { appId: "root", entryPoint: "", plugins: [pluginReact()], routes: [ { path: "/", attr: { data: async () => ({ title: "Home", description: "Welcome to my app", }), render: "ssr", }, }, { path: "/users/:id", attr: { data: async (params) => ({ serviceName: "UserService", serviceMethod: "getUser", args: params, }), render: "streaming", }, }, ], }, ],});package.json:
{ "name": "my-taujs-app", "type": "module", "scripts": { "dev": "tsx server/index.ts", "build": "taujs build", "start": "NODE_ENV=production tsx server/index.ts" }, "dependencies": { "@taujs/react": "^0.1.1", "@taujs/server": "latest", "fastify": "^5.0.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@types/node": "^24.0.7", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.6.0", "tsx": "^4.0.0", "typescript": "^5.5.4", "vite": "^7.1.9" }}Standalone Project Structure (No @taujs/server)
Section titled “Standalone Project Structure (No @taujs/server)”my-app/├── src/│ ├── client/│ │ ├── index.tsx # Client entry│ │ └── App.tsx│ ├── server/│ │ ├── index.ts # Express/Node server│ │ └── render.tsx # SSR renderer│ └── shared/│ └── types.ts├── public/├── dist/ # Build output├── tsconfig.json├── package.json└── vite.config.tssrc/client/index.tsx:
import { hydrateApp } from "@taujs/react";import { App } from "./App";
hydrateApp({ appComponent: <App />,});src/server/render.tsx:
import { createRenderer } from "@taujs/react";import { App } from "../client/App";
export const { renderSSR, renderStream } = createRenderer({ appComponent: ({ location }) => <App location={location} />, headContent: ({ data }) => ` <meta charset="UTF-8"> <title>${data.pageTitle}</title> `,});src/server/index.ts:
import express from "express";import { renderStream } from "./render";
const app = express();
app.use(express.static("dist/client"));
app.get("*", async (req, res) => { const data = { pageTitle: "Home", content: "Hello" };
res.setHeader("Content-Type", "text/html"); res.write(` <!DOCTYPE html> <html> <head> <script>window.__INITIAL_DATA__=${JSON.stringify(data)};</script> </head> <body><div id="root"> `);
const { done } = renderStream( res, { onAllReady: () => { res.write(`</div> <script type="module" src="/assets/index.js"></script> </body></html>`); res.end(); }, }, data, req.url );
await done;});
app.listen(3000);vite.config.ts:
import { defineConfig } from "vite";import react from "@vitejs/plugin-react";
export default defineConfig({ plugins: [react()], build: { outDir: "dist/client", manifest: true, rollupOptions: { input: { main: "src/client/index.tsx", }, }, },});How @taujs/server Uses @taujs/react
Section titled “How @taujs/server Uses @taujs/react”When you use @taujs/server, it handles all the rendering automatically. Here’s what happens behind the scenes:
Request Flow
Section titled “Request Flow”-
Client requests
GET /users/123 -
@taujs/server matches route from your config:
{path: '/users/:id',attr: {data: async (params) => ({ ... }),render: 'streaming',},} -
Data loading - Calls your
datafunction with route params:const initialData = await attr.data({ id: "123" }, context); -
@taujs/react rendering - Server internally uses:
// For render: 'ssr'const { headContent, appHtml } = await renderSSR(initialData, location);// For render: 'streaming'renderStream(res, callbacks, initialData, location, ...); -
HTML assembly - Combines:
- Your HTML template from
index.html - Generated head content
- SSR-rendered app HTML
- Initial data injection:
window.__INITIAL_DATA__ - Client bootstrap script
- Your HTML template from
-
Response - Sends complete HTML to browser
-
Client hydration - Your
entry-client.tsxruns:hydrateApp({ appComponent: <App /> });
What You Define
Section titled “What You Define”entry-server.tsx (per app):
// This is what @taujs/server calls internallyexport const { renderSSR, renderStream } = createRenderer({ appComponent: ({ location }) => <App location={location} />, headContent: ({ data, meta }) => `<title>${data?.title ?? meta.title ?? "App"}</title>`,});entry-client.tsx (per app):
// This runs in the browser after SSRhydrateApp({ appComponent: <App />,});What @taujs/server Handles
Section titled “What @taujs/server Handles”- Declarative testable configuration
- Route matching and parameter extraction
- Data loading (HTTP or service registry)
- Calling your renderer with the right data
- HTML template processing
- Initial data injection
- Static asset management
- Development mode with HMR
- Production manifests and preloading
- CSP headers and nonce generation
- Error handling and fallbacks
- Standardised logging / telemetry
Key Insight
Section titled “Key Insight”With @taujs/server, you:
- Define rendering logic once in
entry-server.tsx - Define hydration once in
entry-client.tsx - Configure routes and data in
taujs.config.ts
Remember to:
- Never call
renderSSRorrenderStreamdirectly - Never manually inject
__INITIAL_DATA__ - Never manually manage HTML templates
The server orchestrates everything based on your declarative configuration.
Advanced Patterns
Section titled “Advanced Patterns”Async Data Loading (Lazy Promise)
Section titled “Async Data Loading (Lazy Promise)”const { renderStream } = createRenderer({ appComponent: ({ location }) => <App location={location} />, headContent: ({ data }) => `<title>${data.pageTitle}</title>`,});
app.get("/products/:id", async (req, res) => { res.setHeader("Content-Type", "text/html");
// Lazy data loading - promise created only when needed const dataLoader = async () => { const product = await db.products.findById(req.params.id); const reviews = await db.reviews.findByProduct(req.params.id); return { product, reviews, pageTitle: product.name }; };
res.write("<!DOCTYPE html><html><head>");
const { done } = renderStream( res, { onHead: (head) => { // Shell ready but data may still be loading res.write(head); res.write('</head><body><div id="root">'); }, onAllReady: (data) => { // All Suspense boundaries resolved res.write(`</div> <script>window.__INITIAL_DATA__=${JSON.stringify(data)};</script> <script type="module" src="/assets/client.js"></script> </body></html>`); res.end(); }, onError: (err) => { console.error(err); res.end("</body></html>"); }, }, dataLoader, // Pass the function, not the result req.url, "/assets/client.js" );
await done;});Custom Logger Integration
Section titled “Custom Logger Integration”import winston from "winston";import { createRenderer } from "@taujs/react";
const logger = winston.createLogger({ level: "info", format: winston.format.json(), transports: [new winston.transports.Console()],});
const customLogger = { info: (message: string, meta?: unknown) => { logger.info(message, meta); }, warn: (message: string, meta?: unknown) => { logger.warn(message, meta); }, error: (message: string, meta?: unknown) => { logger.error(message, meta); }, debug: (category: string, message: string, meta?: unknown) => { logger.debug(`[${category}] ${message}`, meta); }, child: (ctx: Record<string, unknown>) => { return logger.child(ctx); }, isDebugEnabled: (category: string) => { return logger.level === "debug"; },};
const { renderStream } = createRenderer({ appComponent: ({ location }) => <App location={location} />, headContent: ({ data }) => `<title>${data.pageTitle}</title>`, logger: customLogger, enableDebug: true,});Request Cancellation with AbortSignal
Section titled “Request Cancellation with AbortSignal”app.get("*", async (req, res) => { const controller = new AbortController();
// Cancel render if client disconnects res.on("close", () => { controller.abort(); });
// Timeout after 30 seconds const timeout = setTimeout(() => { controller.abort(); }, 30000);
const { done } = renderStream( res, { onAllReady: () => { clearTimeout(timeout); res.end(); }, onError: (err) => { clearTimeout(timeout); console.error(err); }, }, data, req.url, "/assets/client.js", {}, undefined, controller.signal // Pass AbortSignal );
try { await done; } catch (err) { // Handle cancellation if (controller.signal.aborted) { console.log("Render cancelled"); } }});CSP Nonce Support
Section titled “CSP Nonce Support”import crypto from "node:crypto";
app.get("*", (req, res) => { const nonce = crypto.randomBytes(16).toString("base64");
res.setHeader( "Content-Security-Policy", `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';` );
const { done } = renderStream( res, callbacks, data, req.url, "/assets/client.js", {}, nonce // CSP nonce applied to React's renderToPipeableStream );
return done;});Important: The nonce is automatically applied to:
- React’s internal
renderToPipeableStream(via thenonceoption) - All inline scripts you write in
onHead,onAllReady, etc. must also include the nonce:{onAllReady: (data) => {res.write(`<script nonce="${nonce}">window.__INITIAL_DATA__ = ${JSON.stringify(data)};</script>`);},}
Error Boundaries and Recovery
Section titled “Error Boundaries and Recovery”import { Component, ReactNode } from "react";
class ErrorBoundary extends Component< { children: ReactNode }, { hasError: boolean }> { state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
componentDidCatch(error: Error, info: any) { console.error("React Error Boundary caught:", error, info); }
render() { if (this.state.hasError) { return <div>Something went wrong. Please refresh.</div>; } return this.props.children; }}
export function App() { return ( <ErrorBoundary> <YourAppContent /> </ErrorBoundary> );}Multi-Page with Route-Based Data Loading
Section titled “Multi-Page with Route-Based Data Loading”import { matchPath } from "path-to-regexp";
const routes = [ { path: "/", loader: async () => ({ pageTitle: "Home", posts: await db.posts.recent() }), }, { path: "/users/:id", loader: async (params: any) => ({ pageTitle: "User Profile", user: await db.users.findById(params.id), }), }, { path: "/products/:id", loader: async (params: any) => ({ pageTitle: "Product", product: await db.products.findById(params.id), }), },];
app.get("*", async (req, res) => { let data = { pageTitle: "404 Not Found" };
for (const route of routes) { const match = matchPath(route.path, req.path); if (match) { data = await route.loader(match.params); break; } }
const { done } = renderStream(res, callbacks, data, req.url); await done;});TypeScript Support
Section titled “TypeScript Support”The package is written in TypeScript with full type definitions included.
Typed Data Stores
Section titled “Typed Data Stores”interface PageData { pageTitle: string; user: { id: string; name: string; email: string; }; posts: Array<{ id: string; title: string; content: string; }>;}
const { renderSSR, renderStream } = createRenderer<PageData>({ appComponent: ({ location }) => <App location={location} />, headContent: ({ data, meta }) => { // data is typed as PageData return `<title>${data.pageTitle}</title>`; },});
// In your componentfunction MyComponent() { const data = useSSRStore<PageData>(); // data is fully typed return <h1>{data.user.name}</h1>;}Custom Logger Types
Section titled “Custom Logger Types”import type { ServerLogs } from "@taujs/react";
const myLogger: ServerLogs = { info: (message, meta) => { console.log(message, meta); }, warn: (message, meta) => { console.warn(message, meta); }, error: (message, meta) => { console.error(message, meta); }, debug: (category, message, meta) => { console.debug(`[${category}]`, message, meta); },};Production Streaming Checklist
Section titled “Production Streaming Checklist”When using renderStream in production, ensure you have the following in place:
Essential Configuration
Section titled “Essential Configuration”-
Set shell timeout: Configure
shellTimeoutMsappropriately (default: 10s)createRenderer({streamOptions: { shellTimeoutMs: 5000 },}); -
Use cork for write batching: Enabled by default for better performance
streamOptions: {useCork: true;} // default
Request Lifecycle Management
Section titled “Request Lifecycle Management”-
Handle AbortSignal: Pass request cancellation signals
const controller = new AbortController();req.on("close", () => controller.abort());renderStream(res,callbacks,data,location,undefined,{},undefined,controller.signal); -
Set request timeouts: Prevent indefinite hangs
const timeout = setTimeout(() => controller.abort(), 30000);await done.finally(() => clearTimeout(timeout));
Error Handling
Section titled “Error Handling”-
Client disconnects are benign: The renderer automatically treats these as non-fatal:
-
ECONNRESET,EPIPE- network errors -
socket hang up- client closed connection -
aborted- request cancelled -
premature close- stream ended early -
Implement onError callback: Always handle errors gracefully
{ onError: (err) => { logger.error('Stream error', err); if (!res.writableEnded) { res.end('<html><body>Error loading page</body></html>'); } },}Backpressure
Section titled “Backpressure”- Respect backpressure: Return
falsefromonHeadto wait for drain
{ onHead: (head) => { const writeOk = res.write(head); return writeOk; // false means wait for drain },}Best Practices
Section titled “Best Practices”1. Use Streaming for Better TTFB
Section titled “1. Use Streaming for Better TTFB”Streaming SSR provides better Time To First Byte (TTFB) by sending the shell immediately:
// StreamingrenderStream(res, callbacks, data, location);
// Blocking: Static rendering waits for everythingconst { appHtml } = await renderSSR(data, location);2. Handle Data Loading Errors
Section titled “2. Handle Data Loading Errors”Always provide error handlers:
const { done } = renderStream( res, { onError: (err) => { logger.error("SSR error", err); // Send fallback HTML or error page res.statusCode = 500; res.end("<html><body>Error loading page</body></html>"); }, }, dataLoader, location);3. Implement Request Timeouts
Section titled “3. Implement Request Timeouts”Prevent hanging requests:
const controller = new AbortController();const timeout = setTimeout(() => controller.abort(), 10000);
const { done } = renderStream( res, callbacks, data, location, undefined, {}, undefined, controller.signal);
await done.finally(() => clearTimeout(timeout));4. Use Lazy Data Loading for Better Performance
Section titled “4. Use Lazy Data Loading for Better Performance”Defer data fetching until needed:
// Lazy loadingrenderStream( res, callbacks, async () => { return await fetchData(); }, location);
// Blocks before streaming startsconst data = await fetchData();renderStream(res, callbacks, data, location);5. Separate Head Content Generation
Section titled “5. Separate Head Content Generation”Keep head content generation pure and fast:
const { renderStream } = createRenderer({ appComponent: ({ location }) => <App location={location} />, headContent: ({ data, meta }) => { return ` <title>${data.pageTitle}</title> <meta name="description" content="${data.description}"> ${meta.extraMeta || ""} `; },});6. Enable Debug Mode in Development
Section titled “6. Enable Debug Mode in Development”Use detailed logging during development:
const { renderStream } = createRenderer({ appComponent: ({ location }) => <App location={location} />, headContent: ({ data }) => `<title>${data.pageTitle}</title>`, enableDebug: process.env.NODE_ENV === "development",});7. Implement Proper CSR Fallback
Section titled “7. Implement Proper CSR Fallback”Always handle the case where SSR data isn’t available:
hydrateApp({ appComponent: <App />, onHydrationError: (err) => { // Log error but don't crash console.error("Hydration failed, falling back to CSR", err); },});8. Use Type-Safe Data Contracts
Section titled “8. Use Type-Safe Data Contracts”Define clear interfaces between server and client:
export interface AppData { pageTitle: string; user: User | null; posts: Post[];}
// server.tsconst { renderStream } = createRenderer<AppData>({...});
// client.tsconst data = useSSRStore<AppData>();When to Use Direct API vs @taujs/server
Section titled “When to Use Direct API vs @taujs/server”Use @taujs/server (High-Level, Recommended for taujs Ecosystem)
Section titled “Use @taujs/server (High-Level, Recommended for taujs Ecosystem)”When:
- Building a complete application with routing and data loading
- Want declarative configuration via
taujs.config.ts - Need microfrontend architecture support
- Want automatic CSP, authentication, and middleware handling
- Building within the taujs ecosystem
What you get:
- Automatic SSR/streaming setup
- Route-based data loading
- Built-in security (CSP per route)
- Service registry pattern
- Development mode with HMR
- Static asset management
Code example:
// taujs.config.ts - Declarativeexport default defineConfig({ apps: [{ appId: 'root', routes: [{ path: '/:id', attr: { data: async (params) => ({ ... }), render: 'streaming', }, }], }],});
// server.ts - Simpleawait createServer({ fastify, config, serviceRegistry, clientRoot });Use @taujs/react Directly (Low-Level, Framework-Agnostic)
Section titled “Use @taujs/react Directly (Low-Level, Framework-Agnostic)”When:
- Integrating with existing Express/Hono/etc. applications
- Need fine-grained control over rendering
- Building a custom SSR solution
- Don’t need routing or data loading abstractions
- Want minimal dependencies
What you get:
- Direct control over rendering process
- Framework/runtime agnostic
- Minimal abstraction
- Explicit SSR and hydration APIs
Code example:
// Full control over renderingimport { createRenderer } from "@taujs/react";
const { renderStream } = createRenderer({ appComponent: ({ location }) => <App location={location} />, headContent: ({ data }) => `<title>${data.title}</title>`,});
app.get("*", (req, res) => { const { done } = renderStream( res, { onHead: (head) => { /* custom logic */ }, onAllReady: (data) => { /* custom logic */ }, }, data, req.url );});Related Packages
Section titled “Related Packages”@taujs/server- Full-featured Fastify-based server with SSR orchestration
License
Section titled “License”MIT
Contributing
Section titled “Contributing”Issues and pull requests welcome at https://github.com/aoede3/taujs-react