From a12d443a7eb10bfecb0c95174e71c0cc708111c4 Mon Sep 17 00:00:00 2001 From: tzachbon Date: Sat, 25 Jun 2022 18:13:11 +0300 Subject: [PATCH] feat: async rendering with SSR --- packages/app/src/app.tsx | 10 ++++- packages/app/src/components/title.tsx | 5 +++ packages/server/src/render.tsx | 62 +++++++++++++-------------- 3 files changed, 44 insertions(+), 33 deletions(-) create mode 100644 packages/app/src/components/title.tsx diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index f073a45..aa3c271 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -1,5 +1,7 @@ import type React from 'react'; -import { useEffect } from 'react'; +import { lazy, Suspense, useEffect } from 'react'; + +const Title = lazy(() => import('./components/title.js').then((module) => ({ default: module.Title }))); export interface AppProps { text: string; @@ -10,5 +12,9 @@ export const App: React.FC = ({ text }) => { console.log('hello from client!'); }, []); - return

{text}

; + return ( + + {text} + + ); }; diff --git a/packages/app/src/components/title.tsx b/packages/app/src/components/title.tsx new file mode 100644 index 0000000..5c76cf4 --- /dev/null +++ b/packages/app/src/components/title.tsx @@ -0,0 +1,5 @@ +import type { FC } from 'react'; + +export const Title: FC<{ children: string }> = ({ children }) => { + return

{children}

; +}; diff --git a/packages/server/src/render.tsx b/packages/server/src/render.tsx index d4c9bfc..8129904 100644 --- a/packages/server/src/render.tsx +++ b/packages/server/src/render.tsx @@ -1,6 +1,5 @@ import ReactDOMServer from 'react-dom/server'; import { App } from 'app'; -import { on } from 'events'; import type { Express, Request, Response } from 'express'; import fs from 'fs'; import { htmlPath } from './consts.js'; @@ -22,16 +21,7 @@ async function render(_request: Request, response: Response) { 'x-content-type-options': 'nosniff', }); - const html = await fs.promises.readFile(htmlPath, 'utf8'); - - for await (const { chunk, shouldFlush } of renderChunks(html)) { - response.write(chunk); - if (shouldFlush) { - response.flush(); - } - } - - return response.end(); + return renderChunks(response); } catch (error) { console.error(error); return response.status(500).send(error instanceof Error ? error.message : error); @@ -57,30 +47,40 @@ function injectScripts(html: string) { return html; } -async function* renderChunks(html: string): AsyncGenerator<{ chunk: string; shouldFlush: boolean }> { - const abortController = new AbortController(); +async function renderChunks(response: Response) { + const initialHtml = await fs.promises.readFile(htmlPath, 'utf8'); + const html = injectScripts(initialHtml); - html = injectScripts(html); + let didError = false; - const stream = ReactDOMServer.renderToStaticNodeStream(); - const [start, end, openDiv] = [...html.split('
'), '
']; + const [start, _end, openDiv] = [...html.split('
'), '
']; - yield { chunk: start, shouldFlush: false }; - yield { chunk: openDiv, shouldFlush: true }; + const stream = ReactDOMServer.renderToPipeableStream(, { + onShellReady() { + response.statusCode = didError ? 500 : 200; - stream.on('end', () => { - abortController.abort('Finished rendering'); - }); + response.write(start); + response.write(openDiv); - try { - for await (const chunk of on(stream, 'data', { signal: abortController.signal })) { - yield { chunk: String(chunk), shouldFlush: false }; - } - } catch (errorOrAbort) { - if (!abortController.signal.aborted) { - throw errorOrAbort; // It's an error. - } - } + stream.pipe(response); - yield { chunk: end, shouldFlush: true }; + // Missing closing part of the div tag. + }, + onShellError(error) { + response.statusCode = 500; + response.send( + initialHtml.replace( + '', + `` + ) + ); + }, + onError(error) { + didError = true; + response.statusCode = 500; + console.error(error); + }, + }); }