You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
returncontext.render(<AlbumPagealbum={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():
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.
Context
renderWith()is a useful low-level primitive: it creates a request-scoped renderer and installs it ascontext.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:renderToStream()with the current request URL and abort signal<Frame>requests back through the current routerclientEntry()source URLs through the asset serverResponseThis 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:
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 WebResponseobjects rather than having the router implicitly convert returned JSX.Proposed API
Add a conventional
render()middleware alongsiderenderWith():render({ assets })installs the standard typedcontext.render(node, init?)function. Applications without source-servedclientEntry()modules should be able to userender()with no options.The common API should stay intentionally small.
assetsand an optionalonErrorcallback are reasonable initial options. Avoid copying the entireRenderToStreamOptionssurface onto this helper. Applications that need to replace the standard rendering pipeline can userenderWith().Standard behavior owned by
render()The middleware should:
ResponseInittypes oncontext.renderrenderToStream()with the request URL and abort signalcontext.routerGETrequests for frames, preserving authentication/session-relevant headers while overriding or removing headers that are invalid for the internal requestcreateHtmlResponse(stream, init), preserving caller-provided status and headersonError, defaulting to the renderer's existing behaviorThe 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
render(options?)API and its types. KeeprenderWith(factory)andRendererbackward compatible.renderToStream()andcreateHtmlResponse().context.render, response status/headers, doctypes, cancellation, nested and targeted frames, session/auth header forwarding, frame errors, and client-entry URLs.remix/middleware/renderand add the appropriate package change files.render({ assets }).render()as the normal path and move the manualrenderWith()composition to an advanced/custom-renderer example.Acceptance criteria
middleware/render.tsxmodule.context.render(<Page />, init)remains fully typed and returns a WebResponsewith the supplied status and headers.renderWith()continues to support arbitrary request-scoped renderers without a behavior or type regression.