react_server

Transport-neutral SSR and server-function runtime for React Dart.

The package contains the server-function context and registry, the VM-side SSR worker client, and the Node-side React renderer. It has no Shelf or Routed dependency.

Installation

dependencies:
  react_server: ^0.1.0

Choose a separate HTTP adapter:

dependencies:
  react_server_routed: ^0.1.0
  # or
  react_server_shelf: ^0.1.0

Server functions

Create one registry and let generated code populate it:

import 'package:my_app/.generated/server_actions.g.dart';
import 'package:react_server/react_server.dart';

final actions = ServerFunctionRegistry();

void registerActions() {
  registerServerActions(registry: actions);
}

Each handler receives a ServerFunctionContext containing request metadata, authentication state, request headers, deadlines, and cancellation. Concrete server adapters translate their request type into this context.

SSR worker client

The Dart VM server communicates with the generated Node worker through ReactSsrClient:

final ssr = ReactSsrClient(
  endpoint: Uri.parse('http://127.0.0.1:3001/'),
);

final document = await ssr.render(
  component: 'package:my_app/lib/app.dart#App',
  props: {'title': 'Dashboard'},
);

Relative endpoints are resolved only against the baseUri supplied to render or renderStream. When using a relative endpoint, supply a trusted absolute origin; do not derive it from an untrusted request Host header. Using an absolute endpoint avoids the need for a base URI.

ReactSsrDocument contains rendered HTML and serialized props. The HTTP adapter injects both into the configured index template.

On JavaScript runtimes, the same API uses the platform fetch function. This allows a routed_node Node, Bun, or Cloudflare Fetch handler to call a separate SSR service without trying to start a subprocess:

final ssr = ReactSsrClient(
  endpoint: Uri.parse('https://ssr.example.com/'),
);

The endpoint must implement the JSON contract generated by react_tool. The current generated SSR entrypoint is Node-specific (node:http, node:stream, and node:module), so it cannot be bundled directly into a Cloudflare Worker yet. A practical edge deployment is:

Cloudflare Worker (routed_node + react_server_routed)
  ├─ static assets and React actions
  └─ fetch -> regional Node SSR worker (generated by react_tool)

For an experimental Fetch-compatible renderer, configure the SSR build with:

ssr:
  runtime: fetch

That emits a module-style endpoint using React's renderToReadableStream. The target is separate from the Node listener and can be deployed as its own Worker. Streaming through the React server adapter is still pending for this target.

Caching and lifecycle primitives

ReactDocumentCache is an in-process document cache. When documents must be shared between processes or survive restarts, provide a ReactDocumentStore to the selected HTTP adapter. The store receives the document TTL, stale-while- revalidate window, and cache tags, so a Redis, database, disk, or edge-KV implementation can preserve the same application contract.

For a single-host deployment, FileReactDocumentStore provides a restart-safe JSON-on-disk implementation. ReactRouteManifest can supply route-specific TTL, stale windows, and tags:

final app = ReactServerApp(
  // ...
  documentStore: FileReactDocumentStore(Directory('var/react-cache')),
  routeManifest: ReactRouteManifest.fromJson({
    'routes': [
      {
        'pattern': '/news/:slug',
        'ttlSeconds': 30,
        'staleWhileRevalidateSeconds': 300,
        'tags': ['news'],
      },
    ],
  }),
);

Use a database, Redis, or edge-KV implementation of ReactDocumentStore when the application runs on more than one host.

ReactDataCache provides typed data caching with concurrent-load deduplication, stale-while-revalidate, and tag invalidation:

final data = ReactDataCache();
final user = await data.getOrLoad<User>(
  'user:$id',
  () => loadUser(id),
  ttl: const Duration(minutes: 5),
  tags: ['user:$id'],
);

Server functions can schedule non-critical work with ServerFunctionContext.scheduleAfterResponse. The Routed and Shelf adapters drain these callbacks after the action handler completes. Deployment-specific durable background-work guarantees remain the responsibility of the host.

Partial prerendering

Use ReactPartialDocument when a route has a cacheable shell and dynamic regions with different lifetimes. The shell must contain a marker for each region:

ReactPartialDocument(
  shellKey: 'dashboard-shell',
  shell: () => '<main><!--react-partial:summary--></main>',
  regions: [
    ReactPartialRegion(
      key: 'summary',
      ttl: const Duration(seconds: 15),
      render: () => renderSummaryHtml(),
    ),
  ],
);

Pass the descriptor through partialDocument on ReactServerApp or RoutedReactApplication. The adapters retain one ReactDataCache for the application and resolve the shell and every region independently. A short region TTL therefore does not invalidate the shell or unrelated regions.

Node renderer entrypoint

The application's lib/ssr.dart is compiled to JavaScript. It imports hidden generated factories and registries, registers component builders, and calls registerGlobalRenderer. The generated Node runtime then invokes renderToString inside a real React render stack, preserving hooks, contexts, foreign components, suspense, refs, memoization, and error boundaries.

Package boundary

  • HTTP request/response handling belongs in react_server_routed or react_server_shelf.
  • Protocol annotations and browser clients belong in react_actions.
  • Build and worker process orchestration belongs in react_tool.
  • Test fixtures belong in react_testing and compose with the selected server_testing adapter.

Libraries

react_server
Portable server-side runtime for React SSR and server function dispatch.