├── .gitignore ├── README.md ├── package.json ├── public ├── favicons │ └── favicon.ico └── index.html ├── server ├── app.js ├── index.js ├── routes │ ├── api.js │ └── index.js └── universal.js └── src ├── actions └── user.js ├── api.js ├── components └── NoMatch.js ├── config.js ├── containers ├── App.js ├── App.test.js ├── FirstPage.css ├── FirstPage.js ├── FirstPage.test.js ├── SecondPage.css └── SecondPage.js ├── index.css ├── index.js ├── reducers ├── index.js └── user.js ├── routes.js ├── store.js └── types └── user.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Server Side Rendering with Create React App 2 | =========================================== 3 | 4 | I wanted to add server side rendering to create react app, especially after I added an api server side, based on: https://medium.com/@patriciolpezjuri/using-create-react-app-with-react-router-express-js-8fa658bf892d#.sckywa9cy 5 | 6 | Included: redux, react-router 7 | 8 | _Warning: uses react-router 3, v4 just dropped, it breaks everything its kinda annoying_ 9 | 10 | Install 11 | ------- 12 | ```bash 13 | # Why aren't you using yarn already? 14 | npm i -g yarn 15 | yarn 16 | yarn run build 17 | yarn run start:server 18 | ``` 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssr-create-react-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "0.8.5" 7 | }, 8 | "dependencies": { 9 | "body-parser": "^1.16.1", 10 | "compression": "^1.6.2", 11 | "express": "^4.14.0", 12 | "ignore-styles": "^5.0.1", 13 | "import-export": "^1.0.1", 14 | "lodash": "^4.17.4", 15 | "material-ui": "^0.16.7", 16 | "moment": "^2.17.1", 17 | "morgan": "^1.7.0", 18 | "node-fetch": "^1.6.3", 19 | "react": "^15.4.2", 20 | "react-dom": "^15.4.2", 21 | "react-redux": "^5.0.2", 22 | "react-router": "^3.0.2", 23 | "react-sidebar": "^2.2.1", 24 | "react-tap-event-plugin": "^2.0.1", 25 | "redux": "^3.6.0", 26 | "redux-logger": "^2.8.1" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "start:server": "NODE_ENV=development nodemon server", 31 | "build": "react-scripts build", 32 | "test": "react-scripts test --env=jsdom", 33 | "eject": "react-scripts eject" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayroblu/ssr-create-react-app/59f994ea7e4d447ac6980cc270afbc01e8623c7d/public/favicons/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | Server Side Rendering - Create React App 17 | 18 | 19 | 22 |
{{SSR}}
23 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | require('ignore-styles') 2 | require('babel-register')({ ignore: /\/(build|node_modules)\//, presets: ['react-app'] }) 3 | 4 | const bodyParser = require('body-parser') 5 | const compression = require('compression') 6 | const express = require('express') 7 | const morgan = require('morgan') 8 | const path = require('path') 9 | const fs = require('fs') 10 | 11 | const app = express() 12 | 13 | // Support Gzip 14 | app.use(compression()) 15 | 16 | // Suport post requests with body data (doesn't support multipart, use multer) 17 | app.use(bodyParser.json()) 18 | app.use(bodyParser.urlencoded({ extended: false })) 19 | 20 | // Setup logger 21 | app.use(morgan('combined')) 22 | 23 | const index = require('./routes/index') 24 | app.use('/', index) 25 | 26 | // Serve static assets 27 | app.use(express.static(path.resolve(__dirname, '..', 'build'))) 28 | 29 | const api = require('./routes/api') 30 | app.use('/api', api) 31 | 32 | // Always return the main index.html, so react-router render the route in the client 33 | const universalLoader = require('./universal') 34 | app.use('/', universalLoader) 35 | 36 | module.exports = app 37 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const app = require('./app') 2 | 3 | const PORT = process.env.PORT || 3001 4 | 5 | // Why don't I need http createServer 6 | app.listen(PORT, ()=>{ 7 | console.log(`App listening on port ${PORT}!`) 8 | }) 9 | app.on('error', onError) 10 | 11 | function onError(error) { 12 | if (error.syscall !== 'listen') { 13 | throw error; 14 | } 15 | 16 | var bind = typeof port === 'string' 17 | ? 'Pipe ' + port 18 | : 'Port ' + port; 19 | 20 | // handle specific listen errors with friendly messages 21 | switch (error.code) { 22 | case 'EACCES': 23 | console.error(bind + ' requires elevated privileges'); 24 | process.exit(1); 25 | break; 26 | case 'EADDRINUSE': 27 | console.error(bind + ' is already in use'); 28 | process.exit(1); 29 | break; 30 | default: 31 | throw error; 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /server/routes/api.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | router.use(function(req, res, next) { 4 | res.header("Access-Control-Allow-Origin", "*") 5 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept") 6 | next() 7 | }) 8 | 9 | router.get('/', function(req, res, next) { 10 | res.json({}) 11 | }) 12 | 13 | module.exports = router 14 | 15 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | 4 | const universalLoader = require('../universal') 5 | 6 | router.get('/', universalLoader) 7 | 8 | module.exports = router 9 | 10 | 11 | -------------------------------------------------------------------------------- /server/universal.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | 4 | import React from 'react' 5 | import {renderToString} from 'react-dom/server' 6 | import {match, RouterContext} from 'react-router' 7 | 8 | import createRoutes from '../src/routes' 9 | import configureStore from '../src/store' 10 | import {Provider} from 'react-redux' 11 | 12 | const routes = createRoutes({}) 13 | 14 | module.exports = function universalLoader(req, res) { 15 | //res.sendFile(path.resolve(__dirname, '..', 'build', 'index.html')) 16 | const filePath = path.resolve(__dirname, '..', 'build', 'index.html') 17 | 18 | fs.readFile(filePath, 'utf8', (err, htmlData)=>{ 19 | if (err) { 20 | console.error('read err', err) 21 | return res.status(404).end() 22 | } 23 | match({ routes, location: req.url }, (err, redirect, renderProps) => { 24 | if(err) { 25 | console.error('match err', err) 26 | return res.status(404).end() 27 | } else if(redirect) { 28 | res.redirect(302, redirect.pathname + redirect.search) 29 | } else if(renderProps) { 30 | let store = configureStore() 31 | const ReactApp = renderToString( 32 | 33 | 34 | 35 | ) 36 | const RenderedApp = htmlData.replace('{{SSR}}', ReactApp) 37 | res.send(RenderedApp) 38 | } else { 39 | return res.status(404).end() 40 | } 41 | }) 42 | }) 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/actions/user.js: -------------------------------------------------------------------------------- 1 | import { SET, RESET } from '../types/user' 2 | 3 | export function set(payload){ 4 | return { 5 | type: SET 6 | , payload 7 | } 8 | } 9 | 10 | export function reset(){ 11 | return { 12 | type: RESET 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | import { apiUrl } from './config' 2 | import _ from 'lodash' 3 | 4 | class Api { 5 | constructor(options){ 6 | this.apiUrl = apiUrl 7 | this.prefix = '' 8 | if (!options){ 9 | return 10 | } 11 | const {token} = options 12 | this.token = token 13 | } 14 | getJsonHeaders(){ 15 | return { 16 | 'Accept': 'application/json' 17 | } 18 | } 19 | postJsonHeaders(){ 20 | return { 21 | 'Accept': 'application/json' 22 | , 'Content-Type': 'application/json' 23 | } 24 | } 25 | setToken(token){ 26 | this.token = token 27 | } 28 | handleUnauthed(res){ 29 | if (res.status === 401) { 30 | //this.navigateTo('login', {reset: true}) 31 | //db.cleanDb() 32 | return new Promise(()=>{}) 33 | } else { 34 | return res 35 | } 36 | } 37 | _buildQueryString(data){ 38 | return '?' + Object.keys(data).map(d=>d+'='+encodeURIComponent(data[d])) 39 | } 40 | _handleStatus(response){ 41 | const status = response.status 42 | const ok = response.ok 43 | if (status >= 500) { 44 | console.error('Sorry, server had a problem, status code:', status) 45 | return new Promise(()=>{}) 46 | } 47 | const promise = Promise.resolve(response.json()) 48 | if (!ok){ 49 | return promise.then(r=>{ 50 | const message = (r && r.message) || 'No answer from server' 51 | console.error('Sorry, you made a bad request, status code:', status, message) 52 | }) 53 | } else { 54 | return promise 55 | } 56 | } 57 | } 58 | 59 | export class MainApi extends Api{ 60 | constructor(options){ 61 | super(options) 62 | this.prefix = '/api' 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/NoMatch.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | //import './App.css' 3 | 4 | class NoMatch extends Component { 5 | render() { 6 | return ( 7 |
8 | Sorry, page not found 9 |
10 | ) 11 | } 12 | } 13 | 14 | export default NoMatch 15 | 16 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | let apiUrl = 'http://localhost:3001' 2 | if (process.env.NODE_ENV === 'production') { 3 | apiUrl = '' 4 | } 5 | export { 6 | apiUrl 7 | } 8 | -------------------------------------------------------------------------------- /src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import _ from 'lodash' 3 | //import injectTapEventPlugin from 'react-tap-event-plugin' 4 | //import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 5 | 6 | // Needed for onTouchTap 7 | // http://stackoverflow.com/a/34015469/988941 8 | //injectTapEventPlugin() 9 | 10 | export default class App extends Component { 11 | render(){ 12 | // 13 | // 14 | return ( 15 |
16 |

My App Page

17 | {this.props.children} 18 |
19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/containers/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/containers/FirstPage.css: -------------------------------------------------------------------------------- 1 | .bold{ 2 | fontWeight: 'bold' 3 | } 4 | -------------------------------------------------------------------------------- /src/containers/FirstPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import { bindActionCreators } from 'redux' 4 | 5 | import * as userActions from '../actions/user' 6 | import { Link } from 'react-router' 7 | import './FirstPage.css' 8 | 9 | class FirstPage extends Component { 10 | render() { 11 | return ( 12 |
13 |

14 | First Page 15 |

16 |

{'Email: ' + this.props.user.email}

17 | Second 18 |
19 | ) 20 | } 21 | } 22 | 23 | const mapStateToProps = state => ({ 24 | user: state.user 25 | }) 26 | 27 | const mapDispatchToProps = dispatch => ({ 28 | userActions: bindActionCreators(userActions, dispatch) 29 | }) 30 | 31 | export default connect( 32 | mapStateToProps, 33 | mapDispatchToProps 34 | )(FirstPage) 35 | -------------------------------------------------------------------------------- /src/containers/FirstPage.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import FirstPage from './FirstPage' 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div') 7 | ReactDOM.render(, div) 8 | }) 9 | 10 | -------------------------------------------------------------------------------- /src/containers/SecondPage.css: -------------------------------------------------------------------------------- 1 | .bold{ 2 | fontWeight: 'bold' 3 | } 4 | -------------------------------------------------------------------------------- /src/containers/SecondPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import { bindActionCreators } from 'redux' 4 | 5 | import * as userActions from '../actions/user' 6 | import { Link } from 'react-router' 7 | import './SecondPage.css' 8 | 9 | class SecondPage extends Component { 10 | render() { 11 | return ( 12 |
13 |

14 | Second Page 15 |

16 | First 17 |
18 | ) 19 | } 20 | } 21 | 22 | const mapStateToProps = state => ({ 23 | user: state.user 24 | }) 25 | 26 | const mapDispatchToProps = dispatch => ({ 27 | userActions: bindActionCreators(userActions, dispatch) 28 | }) 29 | 30 | export default connect( 31 | mapStateToProps, 32 | mapDispatchToProps 33 | )(SecondPage) 34 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import configureStore from './store' 5 | import './index.css' 6 | 7 | import Routes from './routes' 8 | 9 | // Let the reducers handle initial state 10 | const initialState = {} 11 | const store = configureStore(initialState) 12 | 13 | ReactDOM.render( 14 | 15 | 16 | 17 | , document.getElementById('root') 18 | ) 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | 3 | import user from './user' 4 | 5 | export default combineReducers({ 6 | user 7 | }) 8 | -------------------------------------------------------------------------------- /src/reducers/user.js: -------------------------------------------------------------------------------- 1 | import { SET, RESET } from '../types/user' 2 | 3 | const initialState = { 4 | email: 'user@example.com' 5 | } 6 | 7 | export default function reducer(state=initialState, action) { 8 | switch (action.type) { 9 | case SET: 10 | return {...state, ...action.payload} 11 | case RESET: 12 | return {...initialState} 13 | default: 14 | return state 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Router, Route, IndexRoute, browserHistory } from 'react-router' 3 | 4 | import App from './containers/App' 5 | import FirstPage from './containers/FirstPage' 6 | import SecondPage from './containers/SecondPage' 7 | import NoMatch from './components/NoMatch' 8 | 9 | const Routes = props => { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | 21 | export default Routes 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import reducers from './reducers' 3 | //import createLogger from 'redux-logger' 4 | //import createSagaMiddleware from 'redux-saga' 5 | 6 | //const logger = createLogger() 7 | //const sagaMiddleware = createSagaMiddleware() 8 | 9 | export default function configureStore(initialState = {}) { 10 | // Create the store with two middlewares 11 | const middlewares = [ 12 | // sagaMiddleware 13 | //, logger 14 | ] 15 | 16 | const enhancers = [ 17 | applyMiddleware(...middlewares) 18 | ] 19 | 20 | const store = createStore( 21 | reducers 22 | , initialState 23 | , compose(...enhancers) 24 | ) 25 | 26 | // Extensions 27 | //store.runSaga = sagaMiddleware.run 28 | store.asyncReducers = {} // Async reducer registry 29 | 30 | return store 31 | } 32 | -------------------------------------------------------------------------------- /src/types/user.js: -------------------------------------------------------------------------------- 1 | const prefix = 'USER/' 2 | 3 | export const SET = prefix + 'SET' 4 | export const RESET = prefix + 'RESET' 5 | 6 | --------------------------------------------------------------------------------