Last active
May 2, 2023 06:41
-
-
Save cereallarceny/ee1b86227aabaf4a4b2a3144b84dfaa2 to your computer and use it in GitHub Desktop.
Server-side rendering in Create React App
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const md5File = require('md5-file'); | |
const path = require('path'); | |
// CSS styles will be imported on load and that complicates matters... ignore those bad boys! | |
const ignoreStyles = require('ignore-styles'); | |
const register = ignoreStyles.default; | |
// We also want to ignore all image requests | |
// When running locally these will load from a standard import | |
// When running on the server, we want to load via their hashed version in the build folder | |
const extensions = ['.gif', '.jpeg', '.jpg', '.png', '.svg']; | |
// Override the default style ignorer, also modifying all image requests | |
register(ignoreStyles.DEFAULT_EXTENSIONS, (mod, filename) => { | |
if (!extensions.find(f => filename.endsWith(f))) { | |
// If we find a style | |
return ignoreStyles.noOp(); | |
} else { | |
// If we find an image | |
const hash = md5File.sync(filename).slice(0, 8); | |
const bn = path.basename(filename).replace(/(\.\w{3})$/, `.${hash}$1`); | |
mod.exports = `/static/media/${bn}`; | |
} | |
}); | |
// Set up babel to do its thing... env for the latest toys, react-app for CRA | |
// Notice three plugins: the first two allow us to use import rather than require, the third is for code splitting | |
// Polyfill is required for Babel 7, polyfill includes a custom regenerator runtime and core-js | |
require('@babel/polyfill'); | |
require('@babel/register')({ | |
ignore: [/\/(build|node_modules)\//], | |
presets: ['@babel/preset-env', '@babel/preset-react'], | |
plugins: [ | |
'@babel/plugin-syntax-dynamic-import', | |
'dynamic-import-node', | |
'react-loadable/babel' | |
] | |
}); | |
// Now that the nonsense is over... load up the server entry point | |
require('./server'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Express requirements | |
import path from 'path'; | |
import fs from 'fs'; | |
// React requirements | |
import React from 'react'; | |
import { renderToString } from 'react-dom/server'; | |
import Helmet from 'react-helmet'; | |
import { Provider } from 'react-redux'; | |
import { StaticRouter } from 'react-router'; | |
import { Frontload, frontloadServerRender } from 'react-frontload'; | |
import Loadable from 'react-loadable'; | |
// Our store, entrypoint, and manifest | |
import createStore from '../src/store'; | |
import App from '../src/app/app'; | |
import manifest from '../build/asset-manifest.json'; | |
// Some optional Redux functions related to user authentication | |
import { setCurrentUser, logoutUser } from '../src/modules/auth'; | |
// LOADER | |
export default (req, res) => { | |
/* | |
A simple helper function to prepare the HTML markup. This loads: | |
- Page title | |
- SEO meta tags | |
- Preloaded state (for Redux) depending on the current route | |
- Code-split script tags depending on the current route | |
*/ | |
const injectHTML = (data, { html, title, meta, body, scripts, state }) => { | |
data = data.replace('<html>', `<html ${html}>`); | |
data = data.replace(/<title>.*?<\/title>/g, title); | |
data = data.replace('</head>', `${meta}</head>`); | |
data = data.replace( | |
'<div id="root"></div>', | |
`<div id="root">${body}</div><script>window.__PRELOADED_STATE__ = ${state}</script>` | |
); | |
data = data.replace('</body>', scripts.join('') + '</body>'); | |
return data; | |
}; | |
// Load in our HTML file from our build | |
fs.readFile( | |
path.resolve(__dirname, '../build/index.html'), | |
'utf8', | |
(err, htmlData) => { | |
// If there's an error... serve up something nasty | |
if (err) { | |
console.error('Read error', err); | |
return res.status(404).end(); | |
} | |
// Create a store (with a memory history) from our current url | |
const { store } = createStore(req.url); | |
// If the user has a cookie (i.e. they're signed in) - set them as the current user | |
// Otherwise, we want to set the current state to be logged out, just in case this isn't the default | |
if ('mywebsite' in req.cookies) { | |
store.dispatch(setCurrentUser(req.cookies.mywebsite)); | |
} else { | |
store.dispatch(logoutUser()); | |
} | |
const context = {}; | |
const modules = []; | |
/* | |
Here's the core funtionality of this file. We do the following in specific order (inside-out): | |
1. Load the <App /> component | |
2. Inside of the Frontload HOC | |
3. Inside of a Redux <StaticRouter /> (since we're on the server), given a location and context to write to | |
4. Inside of the store provider | |
5. Inside of the React Loadable HOC to make sure we have the right scripts depending on page | |
6. Render all of this sexiness | |
7. Make sure that when rendering Frontload knows to get all the appropriate preloaded requests | |
In English, we basically need to know what page we're dealing with, and then load all the appropriate scripts and | |
data for that page. We take all that information and compute the appropriate state to send to the user. This is | |
then loaded into the correct components and sent as a Promise to be handled below. | |
*/ | |
frontloadServerRender(() => | |
renderToString( | |
<Loadable.Capture report={m => modules.push(m)}> | |
<Provider store={store}> | |
<StaticRouter location={req.url} context={context}> | |
<Frontload isServer={true}> | |
<App /> | |
</Frontload> | |
</StaticRouter> | |
</Provider> | |
</Loadable.Capture> | |
) | |
).then(routeMarkup => { | |
if (context.url) { | |
// If context has a url property, then we need to handle a redirection in Redux Router | |
res.writeHead(302, { | |
Location: context.url | |
}); | |
res.end(); | |
} else { | |
// Otherwise, we carry on... | |
// Let's give ourself a function to load all our page-specific JS assets for code splitting | |
const extractAssets = (assets, chunks) => | |
Object.keys(assets) | |
.filter(asset => chunks.indexOf(asset.replace('.js', '')) > -1) | |
.map(k => assets[k]); | |
// Let's format those assets into pretty <script> tags | |
const extraChunks = extractAssets(manifest, modules).map( | |
c => `<script type="text/javascript" src="/${c.replace(/^\//, '')}"></script>` | |
); | |
// We need to tell Helmet to compute the right meta tags, title, and such | |
const helmet = Helmet.renderStatic(); | |
// NOTE: Disable if you desire | |
// Let's output the title, just to see SSR is working as intended | |
console.log('THE TITLE', helmet.title.toString()); | |
// Pass all this nonsense into our HTML formatting function above | |
const html = injectHTML(htmlData, { | |
html: helmet.htmlAttributes.toString(), | |
title: helmet.title.toString(), | |
meta: helmet.meta.toString(), | |
body: routeMarkup, | |
scripts: extraChunks, | |
state: JSON.stringify(store.getState()).replace(/</g, '\\u003c') | |
}); | |
// We have all the final HTML, let's send it to the user already! | |
res.send(html); | |
} | |
}); | |
} | |
); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Express requirements | |
import bodyParser from 'body-parser'; | |
import compression from 'compression'; | |
import express from 'express'; | |
import morgan from 'morgan'; | |
import path from 'path'; | |
import forceDomain from 'forcedomain'; | |
import Loadable from 'react-loadable'; | |
import cookieParser from 'cookie-parser'; | |
// Our loader - this basically acts as the entry point for each page load | |
import loader from './loader'; | |
// Create our express app using the port optionally specified | |
const app = express(); | |
const PORT = process.env.PORT || 3000; | |
// NOTE: UNCOMMENT THIS IF YOU WANT THIS FUNCTIONALITY | |
/* | |
Forcing www and https redirects in production, totally optional. | |
http://mydomain.com | |
http://www.mydomain.com | |
https://mydomain.com | |
Resolve to: https://www.mydomain.com | |
*/ | |
// if (process.env.NODE_ENV === 'production') { | |
// app.use( | |
// forceDomain({ | |
// hostname: 'www.mydomain.com', | |
// protocol: 'https' | |
// }) | |
// ); | |
// } | |
// Compress, parse, log, and raid the cookie jar | |
app.use(compression()); | |
app.use(bodyParser.json()); | |
app.use(bodyParser.urlencoded({ extended: false })); | |
app.use(morgan('dev')); | |
app.use(cookieParser()); | |
// Set up homepage, static assets, and capture everything else | |
app.use(express.Router().get('/', loader)); | |
app.use(express.static(path.resolve(__dirname, '../build'))); | |
app.use(loader); | |
// We tell React Loadable to load all required assets and start listening - ROCK AND ROLL! | |
Loadable.preloadAll().then(() => { | |
app.listen(PORT, console.log(`App listening on port ${PORT}!`)); | |
}); | |
// Handle the bugs somehow | |
app.on('error', error => { | |
if (error.syscall !== 'listen') { | |
throw error; | |
} | |
const bind = typeof PORT === 'string' ? 'Pipe ' + PORT : 'Port ' + PORT; | |
switch (error.code) { | |
case 'EACCES': | |
console.error(bind + ' requires elevated privileges'); | |
process.exit(1); | |
break; | |
case 'EADDRINUSE': | |
console.error(bind + ' is already in use'); | |
process.exit(1); | |
break; | |
default: | |
throw error; | |
} | |
}); |
I tried it but I am having a problem, when I run on the web there is an error
Uncaught TypeError: Cannot read property '.css' of undefined
located in: ignore-styles.js
oldHandlers[ext] = require.extensions[ext]
require.extensions[ext] = handler
Can you help me, thank you in advance.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Just to mention, there's a simpler alternative to the SSR approach provided here, if you just need to fix your SEO:
You can use quite straightforward pre-render solutions like Prerender.io or Rendertron. You set them up to work just for social/search engines and they do the rest of the magic without the need to change your application at all.