Skip to content

Instantly share code, notes, and snippets.

@cereallarceny
Created October 12, 2017 20:37
Show Gist options
  • Save cereallarceny/e5bee7cb95ddfe4958f86d6bcda49ae8 to your computer and use it in GitHub Desktop.
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
// 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');
{
"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 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;
// 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;
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;
}
});
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;
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