diff --git a/app/entry.server.tsx b/app/entry.server.tsx
index 4f78e9f..207dbb3 100644
--- a/app/entry.server.tsx
+++ b/app/entry.server.tsx
@@ -1,4 +1,5 @@
-import { renderToString } from 'react-dom/server';
+// Use the server.browser package to be able to use renderToReadableStream also on Node.js/Bun
+import { renderToReadableStream } from 'react-dom/server.browser';
import { RemixServer } from '@remix-run/react';
import type { EntryContext } from '@remix-run/node';
import {
@@ -16,6 +17,9 @@ import PageStyles from './src/PageStyles';
import i18next, { localesDirectory } from './i18next.server';
import i18n from './i18n';
+// Reject all pending promises from handler functions after 5 seconds
+export const streamTimeout = 5000;
+
export default async function handleRequest(
request: Request,
responseStatusCode: number,
@@ -45,18 +49,47 @@ export default async function handleRequest(
- {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
-
+
);
}
- // Render the component to a string.
- const html = renderToString();
+ const controller = new AbortController();
+ let timeout = setTimeout(() => controller.abort(), streamTimeout * 2);
+
+ const stream = await renderToReadableStream(, {
+ signal: controller.signal,
+ });
+ try {
+ await stream.allReady;
+ clearTimeout(timeout);
+ } catch (error) {
+ responseStatusCode = 500;
+ console.error(error);
+ }
+
+ // Read the stream to a string
+ const reader = stream.getReader();
+ const decoder = new TextDecoder();
+ let html = '';
+ await reader.read().then(function processText({
+ done,
+ value,
+ }: ReadableStreamReadResult): void | Promise {
+ if (done) {
+ return;
+ }
+ html += decoder.decode(value);
+ return reader.read().then(processText);
+ });
// Grab the CSS from emotion
const { styles } = extractCriticalToChunks(html);
@@ -77,7 +110,7 @@ export default async function handleRequest(
responseHeaders.set('Content-Type', 'text/html');
- return new Response(`${markup}`, {
+ return new Response(markup, {
status: responseStatusCode,
headers: responseHeaders,
});
diff --git a/env.d.ts b/env.d.ts
index 8d2f951..2d03aeb 100644
--- a/env.d.ts
+++ b/env.d.ts
@@ -1,2 +1,6 @@
///
///
+
+declare module 'react-dom/server.browser' {
+ export * from 'react-dom/server';
+}
diff --git a/tsconfig.json b/tsconfig.json
index 269c0cc..af422c8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,5 +1,10 @@
{
- "include": ["env.d.ts", "**/*.ts", "**/*.tsx"],
+ "include": [
+ "env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ "node_modules/@remix-run/react/future/single-fetch.d.ts"
+ ],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"isolatedModules": true,
diff --git a/vite.config.ts b/vite.config.ts
index 815f1dd..e9be8fc 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -6,7 +6,14 @@ import tsconfigPaths from 'vite-tsconfig-paths';
installGlobals({ nativeFetch: true });
export default defineConfig(({ mode }) => ({
- plugins: [remix(), tsconfigPaths()],
+ plugins: [
+ remix({
+ future: {
+ unstable_singleFetch: true,
+ },
+ }),
+ tsconfigPaths(),
+ ],
build: {
sourcemap: true,
rollupOptions: {