Skip to content

Add conventional render() middleware for Remix UI #11605

Description

@mjackson

Context

renderWith() is a useful low-level primitive: it creates a request-scoped renderer and installs it as context.render. However, Remix applications currently have to build the standard Remix UI renderer themselves.

The default template does this in template/app/middleware/render.tsx, and the Rendering UI guide currently teaches the same pattern. That app-owned module must:

  • call renderToStream() with the current request URL and abort signal
  • resolve <Frame> requests back through the current router
  • construct and forward the appropriate headers for those internal requests
  • resolve clientEntry() source URLs through the asset server
  • infer client-entry export names
  • turn the resulting stream into an HTML Response

This is framework integration code rather than application policy. It is long enough that users will copy it without understanding it, delicate enough for copies to drift, and already inconsistent across the template and demos. In particular, frame URL resolution, target propagation, request-header forwarding, error handling, and client-entry resolution should not be reimplemented by every application.

We should keep rendering explicit at the action boundary:

return context.render(<AlbumPage album={album} />)

We should also keep renderWith(factory) as the generic escape hatch for JSON, email previews, other UI runtimes, and applications with fully custom response policies. The router itself should remain independent of Remix UI, and actions should continue returning explicit Web Response objects rather than having the router implicitly convert returned JSX.

Proposed API

Add a conventional render() middleware alongside renderWith():

import { createAssetServer } from 'remix/assets'
import { render } from 'remix/middleware/render'
import { staticFiles } from 'remix/middleware/static'
import { createRouter } from 'remix/router'

export const assets = createAssetServer({
  // app asset configuration
})

export const router = createRouter({
  middleware: [staticFiles('./public'), render({ assets })],
})

render({ assets }) installs the standard typed context.render(node, init?) function. Applications without source-served clientEntry() modules should be able to use render() with no options.

The common API should stay intentionally small. assets and an optional onError callback are reasonable initial options. Avoid copying the entire RenderToStreamOptions surface onto this helper. Applications that need to replace the standard rendering pipeline can use renderWith().

Standard behavior owned by render()

The middleware should:

  • create one renderer per request and preserve its input and ResponseInit types on context.render
  • call renderToStream() with the request URL and abort signal
  • resolve nested and targeted frames through context.router
  • create safe internal GET requests for frames, preserving authentication/session-relevant headers while overriding or removing headers that are invalid for the internal request
  • propagate the current and top frame URLs correctly
  • resolve source-based client-entry URLs through the supplied asset server while using the same explicit-hash/named-component export rules as the UI renderer
  • return createHtmlResponse(stream, init), preserving caller-provided status and headers
  • report rendering errors through onError, defaulting to the renderer's existing behavior

The implementation should define and test a consistent policy for non-successful frame responses. The default should preserve useful application error content where possible instead of requiring every app to invent an error fragment.

Plan

  • Add the high-level render(options?) API and its types. Keep renderWith(factory) and Renderer backward compatible.
  • Implement the standard Remix UI renderer using renderToStream() and createHtmlResponse().
  • Implement a shared frame resolver that handles nested frame URLs, targets, aborts, relevant request headers, and non-successful responses consistently.
  • Integrate optional asset-server client-entry resolution without duplicating export-name parsing rules.
  • Add focused tests for typed context.render, response status/headers, doctypes, cancellation, nested and targeted frames, session/auth header forwarding, frame errors, and client-entry URLs.
  • Expose the API from remix/middleware/render and add the appropriate package change files.
  • Replace the copied renderer in the default template and representative demos with render({ assets }).
  • Update the render-middleware README and the Rendering UI guide. Teach render() as the normal path and move the manual renderWith() composition to an advanced/custom-renderer example.

Acceptance criteria

  • A normal Remix UI application does not need an app-owned middleware/render.tsx module.
  • The default template configures rendering with one middleware call.
  • context.render(<Page />, init) remains fully typed and returns a Web Response with the supplied status and headers.
  • Frames, request cancellation, generated styles, and hydrated client entries work through the conventional middleware.
  • renderWith() continues to support arbitrary request-scoped renderers without a behavior or type regression.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions