├── .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 |
--------------------------------------------------------------------------------