Skip to content

Latest commit

 

History

History

renderer

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

@ssrx/renderer

The SSRx renderer establishes some patterns to hook into the lifecycle of streaming SSR apps in a framework/library agnostic way. It is client and server framework agnostic, so long as the server runtime supports web streams and AsyncLocalStorage (node 18+, bun, deno, cloudflare, vercel, etc). A handful of renderer plugins for common libraries are maintained in this repo.

See the react-router-kitchen-sink and remix-vite examples for a look at how everything can work together in practice.

Usage

@ssrx/renderer exports a createApp function that allows you to compose all the pieces necessary to render a SSR streamed application.

An example with React (Solid works almost exactly the same):

src/app.tsx
// In this case we're using the `react` renderer, which simply wraps @ssrx/renderer with a react specific stream function
import { createApp } from '@ssrx/react';
import { assetsPlugin } from '@ssrx/renderer/assets';

export const { clientHandler, serverHandler, ctx } = createApp({
  // Usually a router plugin will provide the appRenderer, but you can always provide your own if needed
  appRenderer:
    ({ req }) =>
    () =>
      <div>My App</div>,

  plugins: [
    // If you are also using `@ssrx/vite`, this plugin automatically injects js/css assets into your html stream
    assetsPlugin(),

    // ... your plugins, or 3rd party plugins. More on the plugin shape below
  ],
});
src/entry.client.tsx
import { hydrateRoot } from 'react-dom/client';

import { clientHandler } from './app.tsx';

void hydrate();

async function hydrate() {
  const app = await clientHandler();

  hydrateRoot(document, app());
}
src/server.ts
import { serverHandler } from '~/app.tsx';

export default {
  fetch(req: Request) {
    const { stream, statusCode } = await serverHandler({ req });

    return new Response(stream, { status: statusCode(), headers: { 'Content-Type': 'text/html' } });
  },
};

With the above steps you get a streaming react app with support for lazy asset preloading. However, plugins are where @ssrx/renderer really shines.

Plugins

Plugins can:

  • Hook into the client and server rendering in a standardized way
  • Extend a typesafe ctx object that is made available on the client and the server, even outside of the rendering tree (for example in router loader functions). This is accomplished via a proxy that is exposed on the window in the client context, and via async local storage on the server.

Plugin Shape

See the renderer types file for the full plugin signature.

export type RenderPlugin = {
  id: string;

  /**
   * Called once per request.
   */
  hooksForReq: (props: {
    req: Request;
    meta?: SSRx.ReqMeta;
    renderProps: SSRx.RenderProps;
    ctx: Record<string, unknown>;
  }) => {
    // Called on the client and the server
    common?: {
      /**
       * Extend the app ctx object with additional properties. Consider this "external" context - it is made available
       * to the end application on the server and the client.
       */
      extendAppCtx?: () => Record<string, unknown>;

      /**
       * Wrap the app component with a higher-order component. This is useful for wrapping the app with providers, etc.
       */
      wrapApp?: (props: { children: () => Config['jsxElement'] }) => Config['jsxElement'];

      /**
       * Render the final inner-most app component. Only one plugin may do this - usually a routing plugin.
       */
      renderApp?: () => (() => Config['jsxElement']) | Promise<() => Config['jsxElement']>;
    };

    // Only called on the server
    server?: {
      /**
       * Return a string to emit some HTML into the SSR stream just before the document's closing </head> tag.
       *
       * Triggers once per request.
       */
      emitToDocumentHead?: Promise<string | undefined> | string | undefined;

      /**
       * Return a string to emit into the SSR stream just before the rendering
       * framework (react, solid, etc) emits a chunk of the page.
       *
       * Triggers one or more times per request.
       */
      emitBeforeStreamChunk?: Promise<string | undefined> | string | undefined;

      /**
       * Return a string to emit some HTML to the document body, after the client renderer's first flush.
       *
       * Triggers once per request.
       */
      emitToDocumentBody?: Promise<string | undefined> | string | undefined;

      /**
       * Runs when the stream is done processing.
       */
      onStreamComplete?: Promise<void> | void;
    };
  };
};

Directory

Package Release Notes
@ssrx/renderer @ssrx/renderer version
@ssrx/react @ssrx/react version
@ssrx/remix @ssrx/remix version
@ssrx/solid @ssrx/solid version
@ssrx/streaming @ssrx/streaming version
@ssrx/trpc-react-query @ssrx/trpc-react-query version
@ssrx/plugin-react-router @ssrx/plugin-react-router version
@ssrx/plugin-solid-router @ssrx/plugin-solid-router version
@ssrx/plugin-tanstack-query @ssrx/plugin-tanstack-query version
@ssrx/plugin-tanstack-router @ssrx/plugin-tanstack-router version
@ssrx/plugin-trpc-react @ssrx/plugin-trpc-react version
@ssrx/plugin-unhead @ssrx/plugin-unhead version