diff --git a/reflex/.templates/web/app/entry.server.js b/reflex/.templates/web/app/entry.server.js new file mode 100644 index 00000000000..a6f7729f181 --- /dev/null +++ b/reflex/.templates/web/app/entry.server.js @@ -0,0 +1,68 @@ +import { PassThrough } from "node:stream"; + +import { createReadableStreamFromReadable } from "@react-router/node"; +import { ServerRouter } from "react-router"; +import { isbot } from "isbot"; +import { createElement } from "react"; + +import { renderToPipeableStream } from "react-dom/server.browser"; + +export const streamTimeout = 5_000; + +export default function handleRequest( + request, + responseStatusCode, + responseHeaders, + routerContext, + loadContext, +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + let userAgent = request.headers.get("user-agent"); + + // Ensure requests from bots and SPA Mode renders wait for all content to load before responding + // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation + let readyOption = + (userAgent && isbot(userAgent)) || routerContext.isSpaMode + ? "onAllReady" + : "onShellReady"; + + const { pipe, abort } = renderToPipeableStream( + createElement(ServerRouter, { context: routerContext, url: request.url }), + { + [readyOption]() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }), + ); + + pipe(body); + }, + onShellError(error) { + reject(error); + }, + onError(error) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + }, + ); + + // Abort the rendering stream after the `streamTimeout` so it has time to + // flush down the rejected boundaries + setTimeout(abort, streamTimeout + 1000); + }); +} diff --git a/reflex/.templates/web/vite.config.js b/reflex/.templates/web/vite.config.js index 4499a9393b0..7cd81266b23 100644 --- a/reflex/.templates/web/vite.config.js +++ b/reflex/.templates/web/vite.config.js @@ -30,10 +30,6 @@ export default defineConfig((config) => ({ find: "@", replacement: fileURLToPath(new URL("./public", import.meta.url)), }, - ].concat( - config.command === "build" - ? [{ find: "react-dom/server", replacement: "react-dom/server.node" }] - : [], - ), + ], }, }));