Created
October 12, 2017 20:37
-
-
Save cereallarceny/e5bee7cb95ddfe4958f86d6bcda49ae8 to your computer and use it in GitHub Desktop.
Server-side rendering with create-react-app (Fiber), React Router v4, Helmet, Redux, and Thunk
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
// Ignore those pesky styles | |
require('ignore-styles'); | |
// Set up babel to do its thing... env for the latest toys, react-app for CRA | |
require('babel-register')({ | |
ignore: /\/(build|node_modules)\//, | |
presets: ['env', 'react-app'] | |
}); | |
// 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
{ | |
"name": "cra-ssr", | |
"version": "0.1.0", | |
"homepage": "https://cra-ssr.herokuapp.com/", | |
"private": false, | |
"dependencies": { | |
"babel-cli": "^6.26.0", | |
"babel-preset-env": "^1.6.0", | |
"express": "^4.16.2", | |
"ignore-styles": "^5.0.1", | |
"morgan": "^1.9.0", | |
"react": "^16.0.0", | |
"react-dom": "^16.0.0", | |
"react-helmet": "^5.2.0", | |
"react-redux": "^5.0.6", | |
"react-router-dom": "^4.2.2", | |
"react-router-redux": "^5.0.0-alpha.6", | |
"react-scripts": "1.0.14", | |
"redux": "^3.7.2", | |
"redux-thunk": "^2.2.0" | |
}, | |
"scripts": { | |
"start": "react-scripts start", | |
"build": "react-scripts build", | |
"test": "react-scripts test --env=jsdom", | |
"eject": "react-scripts eject", | |
"serve": "NODE_ENV=development node server/index.js", | |
"deploy": "NODE_ENV=production node server/index.js" | |
} | |
} |
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
// This file includes an optional API common in isomorphic applications | |
// Of course, you should probably spin up your API elsewhere... but you get the idea | |
import express from 'express'; | |
const router = express.Router(); | |
router.use((req, res, next) => { | |
res.header('Access-Control-Allow-Origin', '*'); | |
res.header( | |
'Access-Control-Allow-Headers', | |
'Origin, X-Requested-With, Content-Type, Accept' | |
); | |
next(); | |
}); | |
router.get('/', (req, res, next) => { | |
res.json({}); | |
}); | |
export default router; |
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
// Any route that comes in, send it to the universalLoader | |
import express from 'express'; | |
import universalLoader from '../universal'; | |
const router = express.Router(); | |
router.get('/', universalLoader); | |
export default router; |
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
import bodyParser from 'body-parser'; | |
import compression from 'compression'; | |
import express from 'express'; | |
import morgan from 'morgan'; | |
import path from 'path'; | |
import index from './routes-index'; | |
import api from './routes-api'; | |
import universalLoader from './universal'; | |
// Create our express app (using the port optionally specified) | |
const app = express(); | |
const PORT = process.env.PORT || 3000; | |
// Compress, parse, and log | |
app.use(compression()); | |
app.use(bodyParser.json()); | |
app.use(bodyParser.urlencoded({ extended: false })); | |
app.use(morgan('dev')); | |
// Set up route handling, include static assets and an optional API | |
app.use('/', index); | |
app.use(express.static(path.resolve(__dirname, '../build'))); | |
app.use('/api', api); | |
app.use('/', universalLoader); | |
// Let's rock | |
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; | |
} | |
}); |
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
import { createStore, applyMiddleware, compose } from 'redux'; | |
import { routerMiddleware } from 'react-router-redux'; | |
import thunk from 'redux-thunk'; | |
import createHistory from 'history/createMemoryHistory'; | |
import rootReducer from '../src/modules'; | |
// Create a store and history based on a path | |
const createServerStore = (path = '/') => { | |
const initialState = {}; | |
// We don't have a DOM, so let's create some fake history and push the current path | |
const history = createHistory({ initialEntries: [path] }); | |
// All the middlewares | |
const middleware = [thunk, routerMiddleware(history)]; | |
const composedEnhancers = compose(applyMiddleware(...middleware)); | |
// Store it all | |
const store = createStore(rootReducer, initialState, composedEnhancers); | |
// Return all that I need | |
return { | |
history, | |
store | |
}; | |
}; | |
export default createServerStore; |
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
import path from 'path'; | |
import fs from 'fs'; | |
import React from 'react'; | |
import { renderToString } from 'react-dom/server'; | |
import Helmet from 'react-helmet'; | |
import { Provider } from 'react-redux'; | |
import { ConnectedRouter } from 'react-router-redux'; | |
import { Route } from 'react-router-dom'; | |
import createServerStore from './store'; | |
import App from '../src/containers/app'; | |
// A simple helper function to prepare the HTML markup | |
const prepHTML = (data, { html, head, body }) => { | |
data = data.replace('<html lang="en">', `<html ${html}`); | |
data = data.replace('</head>', `${head}</head>`); | |
data = data.replace('<div id="root"></div>', `<div id="root">${body}</div>`); | |
return data; | |
}; | |
const universalLoader = (req, res) => { | |
// Load in our HTML file from our build | |
const filePath = path.resolve(__dirname, '../build/index.html'); | |
fs.readFile(filePath, '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 and sense of history based on the current path | |
const { store, history } = createServerStore(req.path); | |
// Render App in React | |
const routeMarkup = renderToString( | |
<Provider store={store}> | |
<ConnectedRouter history={history}> | |
<Route component={App} /> | |
</ConnectedRouter> | |
</Provider> | |
); | |
// Let Helmet know to insert the right tags | |
const helmet = Helmet.renderStatic(); | |
// Form the final HTML response | |
const html = prepHTML(htmlData, { | |
html: helmet.htmlAttributes.toString(), | |
head: | |
helmet.title.toString() + | |
helmet.meta.toString() + | |
helmet.link.toString(), | |
body: routeMarkup | |
}); | |
// Up, up, and away... | |
res.send(html); | |
}); | |
}; | |
export default universalLoader; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment