Capítulo 7 de 24

Chapter 7: SSR (File-Based)

Core Idea

This example shows the minimal server-rendering setup for TanStack Router with file-based routing: an Express server hands each request to a router-aware SSR handler that renders the full HTML string before sending it to the client. It is the baseline SSR pattern that the streaming variant (Chapter 8) builds on.

Setup

  • Routing style: file-based (routeTree.gen generated by @tanstack/router-plugin)
  • Key dependencies: @tanstack/react-router, @tanstack/router-plugin, express, compression, get-port
  • Structure: server.js (Express entry, dev uses Vite middleware, prod serves ./dist/client), src/router.tsx (shared router factory), src/entry-server.tsx (per-request render), src/routes/posts/$postId.tsx (a route with both an awaited loader value and a deferred promise).

Code Example

// src/entry-server.tsx
import { pipeline } from 'node:stream/promises'
import {
  RouterServer,
  createRequestHandler,
  renderRouterToString,
} from '@tanstack/react-router/ssr/server'
import { createRouter } from './router'

export async function render({ req, res, head }) {
  const url = new URL(req.originalUrl || req.url, 'http://localhost:3000').href
  const request = new Request(url, { method: req.method, headers: /* copied from req.headers */ new Headers() })

  const handler = createRequestHandler({
    request,
    createRouter: () => {
      const router = createRouter()
      router.update({ context: { ...router.options.context, head } })
      return router
    },
  })

  const response = await handler(({ responseHeaders, router }) =>
    renderRouterToString({
      responseHeaders,
      router,
      children: <RouterServer router={router} />,
    }),
  )

  res.statusMessage = response.statusText
  res.status(response.status)
  response.headers.forEach((value, name) => res.setHeader(name, value))
  return pipeline(response.body, res)
}
  • What it demonstrates: createRequestHandler bridges a Fetch Request/Response to Express, and renderRouterToString produces the complete HTML in one pass (no streaming) via RouterServer.

Key Takeaways

  1. createRouter() must be a fresh factory function per request (src/router.tsx exports it), so each SSR pass gets an isolated router instance, no state leaks between requests.
  2. renderRouterToString waits for the whole tree (including awaited loader data) before responding; a route can still hold a genuinely deferred promise (like commentsPromise in posts/$postId.tsx), which resolves client-side after hydration via <Await>.
  3. The Vite dev/prod split in server.js (vite.ssrLoadModule vs import('./dist/server/entry-server.js')) is the standard pattern for SSR apps built with Vite.
  4. context: { head: '' } on the router lets entry-server.tsx inject Vite's extracted <head> content per request.

Connects To

  • ch008-basic-ssr-streaming-file-based: Same route (posts/$postId.tsx) and same router setup, but swaps renderRouterToString for renderRouterToStream, streaming the response instead of buffering it fully.