React Server Side Components: Difference between revisions
Jump to navigation
Jump to search
Line 11: | Line 11: | ||
*Client Entry Point (for testing) | *Client Entry Point (for testing) | ||
*Server Entry Point | *Server Entry Point | ||
*Amend Index.html | |||
*Packages.json scripts | *Packages.json scripts | ||
==Express Server== | ==Express Server== | ||
Line 174: | Line 175: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==Amend Index.html== | |||
Change the index.html | Change the index.html | ||
<syntaxhighlight | <syntaxhighlight lang="html"> | ||
<!doctype html> | <!doctype html> | ||
<html lang="en"> | <html lang="en"> |
Revision as of 23:18, 23 September 2023
Introduction
React by default downloads your bundles and generates HTML on the client. This approach change the process to generate HTML on the Server and send it to the client. This should.
- Improve SEO as the HTML can now by read on the server by the robots
- Improve the user experience as the HTML is rendered quicker
- Maybe improve performance
- Make the build a bit more complex
Server Side Rendering (SSR)
This is where the who of application is rendered on the server. I am using vite so the setup of this is quite easy.You basically create an express service and server pages from this. The example I found was [here]
- Express Server
- Client Entry Point (for testing)
- Server Entry Point
- Amend Index.html
- Packages.json scripts
Express Server
For the main part I created the Express Server
import type { Request, Response, NextFunction } from 'express'
import fs from 'fs/promises'
import path from 'path'
import { fileURLToPath } from 'url'
import express from 'express'
import compression from 'compression'
import serveStatic from 'serve-static'
import { createServer as createViteServer } from 'vite'
const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const resolve = (p: string) => path.resolve(__dirname, p)
const getStyleSheets = async () => {
try {
const assetpath = resolve('dist/assets')
const files = await fs.readdir(assetpath)
const cssAssets = files.filter(l => l.endsWith('.css'))
const allContent = []
for (const asset of cssAssets) {
const content = await fs.readFile(path.join(assetpath, asset), 'utf-8')
allContent.push(`<style type="text/css">${content}</style>`)
}
return allContent.join('\n')
} catch {
return ''
}
}
async function createServer(isProd = process.env.NODE_ENV === 'production') {
const app = express()
// Create Vite server in middleware mode and configure the app type as
// 'custom', disabling Vite's own HTML serving logic so parent server
// can take control
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'custom',
logLevel: isTest ? 'error' : 'info',
})
// use vite's connect instance as middleware
// if you use your own express router (express.Router()), you should use router.use
app.use(vite.middlewares)
const requestHandler = express.static(resolve('assets'))
app.use(requestHandler)
app.use('/assets', requestHandler)
if (isProd) {
app.use(compression())
app.use(
serveStatic(resolve('dist/client'), {
index: false,
})
)
}
const stylesheets = getStyleSheets()
app.use('*', async (req: Request, res: Response, next: NextFunction) => {
const url = req.originalUrl
try {
// 1. Read index.html
let template = await fs.readFile(
isProd ? resolve('dist/client/index.html') : resolve('index.html'),
'utf-8'
)
// 2. Apply Vite HTML transforms. This injects the Vite HMR client, and
// also applies HTML transforms from Vite plugins, e.g. global preambles
// from @vitejs/plugin-react
template = await vite.transformIndexHtml(url, template)
// 3. Load the server entry. vite.ssrLoadModule automatically transforms
// your ESM source code to be usable in Node.js! There is no bundling
// required, and provides efficient invalidation similar to HMR.
let productionBuildPath = path.join(__dirname, './dist/server/entry-server.mjs')
let devBuildPath = path.join(__dirname, './src/client/entry-server.tsx')
const { render } = await vite.ssrLoadModule(isProd ? productionBuildPath : devBuildPath)
// 4. render the app HTML. This assumes entry-server.js's exported `render`
// function calls appropriate framework SSR APIs,
// e.g. ReactDOMServer.renderToString()
const appHtml = await render(url)
const cssAssets = isProd ? '' : await stylesheets
// 5. Inject the app-rendered HTML into the template.
const html = template.replace(`<!--app-html-->`, appHtml).replace(`<!--head-->`, cssAssets)
// 6. Send the rendered HTML back.
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (e: any) {
!isProd && vite.ssrFixStacktrace(e)
console.log(e.stack)
// If an error is caught, let Vite fix the stack trace so it maps back to
// your actual source code.
vite.ssrFixStacktrace(e)
next(e)
}
})
const port = process.env.PORT || 7456
app.listen(Number(port), '0.0.0.0', () => {
console.log(`App is listening on http://localhost:${port}`)
})
}
createServer()
Client Entry Point (for testing)
Created Entry Points, one the client in case we want to debug
import React from 'react'
import { createRoot, hydrateRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import { App } from './App'
import './index.css'
const container = document.getElementById('app')
const FullApp = () => (
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
)
if (import.meta.hot || !container?.innerText) {
const root = createRoot(container!)
root.render(<FullApp />)
} else {
hydrateRoot(container, <FullApp />)
}
Server Entry Point
And a server entry point
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import { StaticRouter } from 'react-router-dom/server'
import { App } from './App'
import './index.css'
export function render(url: string): string {
// eslint-disable-next-line import/no-named-as-default-member
return ReactDOMServer.renderToString(
<React.StrictMode>
<StaticRouter location={url}>
<App />
</StaticRouter>
</React.StrictMode>
)
}
Amend Index.html
Change the index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="app" style="height: 100%; width: 100%"><!--app-html--></div>
<script type="module" src="/src/client/entry-client.tsx"></script>
</body>
</html>
Packages.json scripts
And here is the packages.json
...
"scripts": {
"dev:server": "nodemon --watch server.ts --exec 'ts-node --esm server.ts'",
"dev:client": "yarn build:client && vite --config vite.config.ts dev",
"build": "rimraf dist && tsc -p tsconfig.prod.json && yarn build:client && yarn build:server && yarn copy-files",
"build:client": "vite build --outDir dist/client --ssrManifest",
"build:server": "vite build --ssr src/client/entry-server.tsx --outDir dist/server",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"format": "prettier --write 'src/**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc",
"preview": "vite preview"
},
...