Valerii Iatsko

Full Stack Developer

Hi! My name is Valerii Iatsko.

I'm a Full Stack Developer and the author of this blog.

→ Follow on Twitter

React for Beginners - Episode 2 - Creating isomorphic React+Redux App and deploying it on Heroku

1

Hey, guys! Sorry for the long delay after previous article. I was thinking a lot and, actually, learned a lot about building isomorphic apps in React+Redux. I’ve tried many approaches and want to share one of them with you, which I find the most straightforward and easy.

Also, I’ll show you how to run your isomorphic app on Heroku, but this step is optional. :)

-> Most of the stuff here came up after I invested some time investigating https://github.com/erikras/react-redux-universal-hot-example works and how to build isomorphic app using the same approach step-by-step.

Let’s begin!

Bootstrapping (Github, Editorconfig, Eslint)

(You can skip this part if you’re fine with configuring eslint and editorconfig yourself)

I’ll do all the job here in https://github.com/codingbox/isomorphic-react-redux-tutorial repository.

First off, let’s create an empty NodeJS project on github and pick Node .gitgnore and MIT license (the last one doesn’t really matter).

2

Clone it afterwards and create empty package.json file using command:

npm init

Mine looks like this:

{
  "name": "isomorphic-react-redux-tutorial",
  "version": "0.1.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/codingbox/isomorphic-react-redux-tutorial.git"
  },
  "author": "Valerii Iatsko <dwr@codingbox.io>",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/codingbox/isomorphic-react-redux-tutorial/issues"
  },
  "homepage": "https://github.com/codingbox/isomorphic-react-redux-tutorial#readme"
}

Let’s add .editorconfig to our project to enforce 2 spaces (or whatever you prefer):

root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 2

This is very useful especially when you have different code style rules at your work (I do). So I’m enforcing settings per project using .editorconfig and I’d recommend you to do so too.

Now, let’s install eslint (I prefer airbnb’s one). To install it, it’s enough to just run:

(
  export PKG=eslint-config-airbnb;
  npm info "$PKG" peerDependencies --json | command sed 's/[\{\},]//g ; s/: /@/g' | xargs npm install --save-dev "$PKG"
)

It will automatically add required dependencies to package.json.

After that, it’s enough to just create .eslintrc.js:

module.exports = {
  extends: 'airbnb',
};

Your project directory should look like this after everything described in this section:

https://github.com/codingbox/isomorphic-react-redux-tutorial/tree/ec3a024a9224f28ddf5539f657475e5b78d29f83

3

A little bit about isomorphic application structure

Isomorphic app in general should, in the perfect case, consist of two parts:

  • API service
  • Template Rendering service

Why? Simply to separate workload and have ability to scale services as needed.

In development mode, here webpack dev server could be added.

Let’s create the basic services launch files, before doing this, install necessary dependencies:

npm install express http-proxy webpack --save
npm install webpack-dev-middleware webpack-hot-middleware --save-dev

Note, that although webpack is in dependencies, we are not going to run webpack in production as it is, but we will use it on our pre-production/production server to build the bundle, so it’s required.

In order to make sure every service knows about another one, we’ll need a config, I’d suggest to go with following:

module.exports = {
  port: process.env.PORT || 3000,
  apiHost: 'localhost',
  apiPort: 3001,
  webpackHost: 'localhost',
  webpackPort: 3002,
};

Create this file as ./config/env.js.

4

Now let’s create our services launchers:

./bin/api.js:

/* eslint no-console: 0 */
import express from 'express';
import { apiPort } from '../config/env';
const app = express();
app.get('/api', (req, res) => {
  res.send('Hello, world!');
});
app.listen(apiPort, (err) => {
  if (err) {
    console.error(err);
  } else {
    console.info(`Api listening on port ${apiPort}!`);
  }
});

./bin/server.js:

/* eslint no-console: 0 */
import express from 'express';
import http from 'http';
import httpProxy from 'http-proxy';
import path from 'path';
import { port, apiHost, apiPort } from '../config/env';
const targetUrl = `http://${apiHost}:${apiPort}`;
const app = express();
const server = new http.Server(app);
const proxy = httpProxy.createProxyServer({
  target: targetUrl,
  ws: true,
});
app.use('/', express.static(path.resolve(__dirname, '../public')));
app.use('/api', (req, res) => {
  proxy.web(req, res, { target: `${targetUrl}/api` });
});
server.on('upgrade', (req, socket, head) => {
  proxy.ws(req, socket, head);
});
proxy.on('error', (error, req, res) => {
  if (error.code !== 'ECONNRESET') {
    console.error('proxy error', error);
  }
if (!res.headersSent) {
    res.writeHead(500, { 'content-type': 'application/json' });
  }
const json = { error: 'proxy_error', reason: error.message };
res.end(JSON.stringify(json));
});
app.listen(port, (err) => {
  if (err) {
    console.error(err);
  } else {
    console.info(`Server listening on port ${port}!`);
  }
});

./bin/webpack-dev-server.js

/* eslint import/no-extraneous-dependencies: 0, no-console: 0 */
import express from 'express';
import webpack from 'webpack';
const { host, port, webpackPort } = require('../config/env');
const webpackConfig = {};
const compiler = webpack(webpackConfig);
const serverOptions = {
  contentBase: `http://${host}:${port}`,
  quiet: true,
  noInfo: true,
  hot: true,
  inline: true,
  lazy: false,
  publicPath: webpackConfig.output.publicPath,
  headers: { 'Access-Control-Allow-Origin': '*' },
  stats: { colors: true },
};
const app = express();
app.use(require('webpack-dev-middleware')(compiler, serverOptions));
app.use(require('webpack-hot-middleware')(compiler));
app.listen(webpackPort, (err) => {
  if (err) {
    console.error(err);
  } else {
    console.info(`Webpack development server listening on port ${webpackPort}`);
  }
});

Note that webpackConfig is currently an empty project — we’ll fix this later. :)

Let’s make all these services starting and playing well together!

We’ll need another dependency, called concurrently:

npm install concurrently --save

One last thing, before we’ll make this service playing together — as you can see, we are using es6 modules, these are not supported by node yet. So we’ll need to use babel-node executable to run these service also.

Make sure to install it (we’ll also install react & webpack’s loader right away):

npm install babel-loader babel-cli babel-preset-latest babel-preset-react babel-preset-stage-0 --save

To make this babel plugins working, we’ll need the following .babelrc in the root directory:

{
  "presets": [
    "latest",
    "stage-0",
    "react"
  ]
}

After that, appending the following script runners to package.json would be enough:

  "scripts": {
    "start": "concurrently --kill-others \"npm run start-api\" \"npm run start-server\" \"npm run webpack-dev-server\"",
    "start-api": "NODE_ENV=development babel-node ./bin/api.js",
    "start-server": "NODE_ENV=development babel-node ./bin/server.js",
    "webpack-dev-server": "NODE_ENV=development babel-node ./bin/webpack-dev-server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

So the whole package.json will look like this:

{
  "name": "isomorphic-react-redux-tutorial",
  "version": "0.1.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "concurrently --kill-others \"npm run start-api\" \"npm run start-server\" \"npm run webpack-dev-server\"",
    "start-api": "NODE_ENV=development babel-node ./bin/api.js",
    "start-server": "NODE_ENV=development babel-node ./bin/server.js",
    "webpack-dev-server": "NODE_ENV=development babel-node ./bin/webpack-dev-server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/codingbox/isomorphic-react-redux-tutorial.git"
  },
  "author": "Valerii Iatsko <dwr@codingbox.io>",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/codingbox/isomorphic-react-redux-tutorial/issues"
  },
  "homepage": "https://github.com/codingbox/isomorphic-react-redux-tutorial#readme",
  "devDependencies": {
    "eslint": "^3.7.0",
    "eslint-config-airbnb": "^12.0.0",
    "eslint-plugin-import": "^1.16.0",
    "eslint-plugin-jsx-a11y": "^2.2.2",
    "eslint-plugin-react": "^6.3.0",
    "webpack-dev-middleware": "^1.8.3",
    "webpack-hot-middleware": "^2.12.2"
  },
  "dependencies": {
    "babel-cli": "^6.16.0",
    "babel-loader": "^6.2.5",
    "babel-preset-latest": "^6.16.0",
    "babel-preset-react": "^6.16.0",
    "babel-preset-stage-0": "^6.16.0",
    "concurrently": "^3.1.0",
    "express": "^4.14.0",
    "http-proxy": "^1.15.1",
    "webpack": "^1.13.2"
  }
}

After that, if you’ll try npm start, you’ll see that everything’s working except for webpack:

5

Let’s configure it.

The code we currently have is following https://github.com/codingbox/isomorphic-react-redux-tutorial/tree/17e6a9fd7029c56f04b7c42d5fc74f5142387e14

Configuring WebPack

Remember in ./bin/webpack-dev-server.js we left the config as an empty object? That’s it! We now need to come up with the config.

Let’s create an empty webpack’s entry point to work with.

At this moment, “Hello, world!” would be good enough:

./src/index.js

console.log('Hello, world');

Let’s create development webpack’s config to use it:

./webpack/dev.config.js

const path = require('path');
const webpack = require('webpack');
const assetsPath = path.resolve(__dirname, '../public/assets');
const { webpackHost, webpackPort } = require('../config/env');
module.exports = {
  devtool: 'inline-source-map',
  context: path.resolve(__dirname, '..'),
  entry: {
    main: [
      `webpack-hot-middleware/client?path=http://${webpackHost}:${webpackPort}/__webpack_hmr`,
      './src/index.js',
    ],
  },
  output: {
    path: assetsPath,
    filename: '[name].js',
    chunkFilename: '[name]-[chunkhash].js',
    publicPath: `http://${webpackHost}:${webpackPort}/assets/`,
  },
  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        exclude: /(node_modules|bower_components)/,
        loader: 'babel',
      },
    ],
  },
  progress: true,
  resolve: {
    modulesDirectories: [
      'node_modules',
      'src',
    ],
    extensions: ['', '.json', '.js', '.jsx'],
  },
  plugins: [
    // hot reload
    new webpack.HotModuleReplacementPlugin(),
    new webpack.IgnorePlugin(/webpack-stats\.json$/),
  ],
};

And let’s fix ./bin/webpack-dev-server.js to use real entry point now:

const webpackConfig = require('../webpack/dev.config');

Now, after doing

npm start

You’ll now get everything running:

6

If not, check with our github repository for this point https://github.com/codingbox/isomorphic-react-redux-tutorial/tree/1641443ca26d506378b07eb8d3e6f89cc9fa0364

If everything’s working fine, you’ll get hello world messages accessing http://localhost:3000/api and http://localhost:3000/ (this will confirm server & api are working correctly).

React!

Now, to the favourite part of many people :) Let’s add React to the project.

I won’t separate dependencies step-by-step, so let’s install them all at once:

npm install react react-dom serialize-javascript redux react-redux react-router react-router-redux pretty-error --save

This is a brief explanation of what belongs to what:

react & react-dom are related to React

serialize-javascript will be used to serialize the store for future deserialization on client

redux & react-redux — Redux

react-router & react-router-redux — routing libraries

pretty-error — for much more neat error messages

Let’s create a simple layout file with html markup, in which we’ll be inserting React component (and this layout will also be written in React):

./src/layouts/Default.jsx

/* eslint react/prefer-stateless-function: 0, react/no-danger: 0, react/forbid-prop-types: 0 */
import React from 'react';
import ReactDOM from 'react-dom/server';
import serialize from 'serialize-javascript';
import { webpackHost, webpackPort } from '../../config/env';
export default class Default extends React.Component {
  render() {
    const { component, store } = this.props;
    const content = component ? ReactDOM.renderToString(component) : '';
  return (
      <html lang="en">
        <head>
          <title>Hello, world!</title>
        </head>
        <body>
          <div id="root" dangerouslySetInnerHTML={{ __html: content }} />
          <script
            dangerouslySetInnerHTML={{ __html: `window.__data=${serialize(store.getState())};` }}
            charSet="UTF-8"
          />
          <script
            src={
              process.env.NODE_ENV === 'development' ?
              `http://${webpackHost}:${webpackPort}/assets/main.js` :
              '/assets/main.js'
            }
            charSet="UTF-8"
          />
        </body>
      </html>
    );
  }
}
Default.propTypes = {
  component: React.PropTypes.node,
  store: React.PropTypes.object,
};

This layout is pretty much self-explanational, I hope. We’ll need to pass the root component & store to this layout later.

We’ll need to upgrade ./bin/server.js quiet a bit afterwards:

/* eslint no-console: 0, react/jsx-filename-extension: 0 */
import express from 'express';
import http from 'http';
import httpProxy from 'http-proxy';
import path from 'path';
import PrettyError from 'pretty-error';
import React from 'react';
import ReactDOM from 'react-dom/server';
import { match, RouterContext } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
import createHistory from 'react-router/lib/createMemoryHistory';
import { Provider } from 'react-redux';
import {
  createStore,
} from '../src/redux/createStore';
import getRoutes from '../src/routes';
import Default from '../src/layouts/Default';
import { port, apiHost, apiPort } from '../config/env';
const targetUrl = `http://${apiHost}:${apiPort}`;
const pretty = new PrettyError();
const app = express();
const server = new http.Server(app);
const proxy = httpProxy.createProxyServer({
  target: targetUrl,
  ws: true,
});
app.use('/', express.static(path.resolve(__dirname, '../public')));
app.use('/api', (req, res) => {
  proxy.web(req, res, { target: `${targetUrl}/api` });
});
app.get('/', (req, res) => {
  res.send('Hello from server!');
});
server.on('upgrade', (req, socket, head) => {
  proxy.ws(req, socket, head);
});
proxy.on('error', (error, req, res) => {
  if (error.code !== 'ECONNRESET') {
    console.error('proxy error', error);
  }
if (!res.headersSent) {
    res.writeHead(500, { 'content-type': 'application/json' });
  }
const json = { error: 'proxy_error', reason: error.message };
res.end(JSON.stringify(json));
});
app.use((req, res) => {
  const memoryHistory = createHistory(req.originalUrl);
  const store = createStore(memoryHistory);
  const history = syncHistoryWithStore(memoryHistory, store);
  function hydrateOnClient() {
    res.send(`<!doctype html>${ReactDOM.renderToString(<Default store={store} />)}`);
  }
  match({ history, routes: getRoutes(store), location: req.originalUrl },
  (error, redirectLocation, renderProps) => {
    if (redirectLocation) {
      res.redirect(redirectLocation.pathname + redirectLocation.search);
    } else if (error) {
      console.error('ROUTER ERROR:', pretty.render(error));
      res.status(500);
      hydrateOnClient();
    } else if (renderProps) {
      const component = (
        <Provider store={store} key="provider">
          <RouterContext {...renderProps} />
        </Provider>
      );
      res.status(200);
      global.navigator = { userAgent: req.headers['user-agent'] };
      res.send(`<!doctype html>${ReactDOM.renderToStaticMarkup(<Default component={component} store={store} />)}`);
    } else {
      res.status(404).send('Not found');
    }
  });
});
app.listen(port, (err) => {
  if (err) {
    console.error(err);
  } else {
    console.info(`Server listening on port ${port}!`);
  }
});

As you can see we have two things missing in order to make it working:

7

import {
  createStore,
} from '../src/redux/createStore';
import getRoutes from '../src/routes';

Let’s start with routes.

To begin with, let’s create 3 containers: App (holding all child elements), Home, NotFound:

./src/containers/App.jsx

/* eslint react/prefer-stateless-function: 0, react/forbid-prop-types: 0 */
import React from 'react';
export default class App extends React.Component {
  render() {
    return <div>{this.props.children}</div>;
  }
}
App.propTypes = {
  children: React.PropTypes.any,
};

./src/containers/Home.jsx

/* eslint react/prefer-stateless-function: 0 */
import React from 'react';
export default class Home extends React.Component {
  render() {
    return <div>Home</div>;
  }
}

./src/containers/NotFound.jsx

/* eslint react/prefer-stateless-function: 0 */
import React from 'react';
export default class NotFound extends React.Component {
  render() {
    return <div>NotFound</div>;
  }
}

And an index.js file in containers directory to export all of these:

export App from './App';
export Home from './Home';
export NotFound from './NotFound';

Now, let’s create route file for containers:

./src/routes.js

/* eslint react/jsx-filename-extension: 0 */
import React from 'react';
import { IndexRoute, Route } from 'react-router';
import {
  App,
  Home,
  NotFound,
} from './containers';
export default (store) => { // eslint-disable-line
  return (
    <Route path="/" component={App}>
      { /* Home (main) route */ }
      <IndexRoute component={Home} />
      { /* Catch all route */ }
      <Route path="*" component={NotFound} status={404} />
    </Route>
  );
};

If you want to learn more about react-router-redux, visit it’s official github page https://github.com/reactjs/react-router-redux

Beside routing, we have Redux stuff left.

We already have one reducer (routing), so let’s use it :)

Create ./src/reducers/index.js file (which will be used to combine all our reducers and this is the place where you should be adding your reducers in future):

import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
module.exports = combineReducers({
  routing: routerReducer,
});

Our ./src/redux/createStore.js will be a bit more complicated than you might be expecting it to be:

/* eslint import/no-extraneous-dependencies: 0, global-require: 0 */
import { createStore as _createStore, applyMiddleware } from 'redux';
import { routerMiddleware } from 'react-router-redux';
function createStoreWithReducer(history, data, reducer) {
  const reduxRouterMiddleware = routerMiddleware(history);
  const middleware = [
    reduxRouterMiddleware,
  ];
  const finalCreateStore = applyMiddleware(...middleware)(_createStore);
  const store = finalCreateStore(reducer, data);
  if (process.env.NODE_ENV === 'development' && module.hot) {
    module.hot.accept('../reducers', () => {
      store.replaceReducer(require('../reducers'));
    });
  }
  return store;
}
function createStore(history, data) {
  return createStoreWithReducer(history, data, require('../reducers'));
}
module.exports = {
  createStore,
};

It’s done this way to provide you a bit of flexibility. So you can create createAdminStore in future and use admin reducers (in future, I’ll push article on how to create admin panel).

I wanted to mention, that this particular createStore implementation is what I found in erikras’s boilerplate (it has not been updated for a long time, but you might want to visit that boilerplate’s github repo for the source of future inspiration).

At last, let’s update our bundle’s entry point located in ./src/index.js:

/* eslint no-underscore-dangle: 0, react/jsx-filename-extension: 0, no-console: 0 */
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { Router, browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
import { createStore } from './redux/createStore';
import getRoutes from './routes';
const dest = global.document.getElementById('root');
const store = createStore(browserHistory, global.__data);
const history = syncHistoryWithStore(browserHistory, store);
const component = (
  <Router history={history}>
    {getRoutes(store)}
  </Router>
);
ReactDOM.render(
  <Provider store={store} key="provider">
    {component}
  </Provider>,
  dest
);
if (process.env.NODE_ENV !== 'production') {
  global.React = React; // enable debugger
if (!dest || !dest.firstChild || !dest.firstChild.attributes || !dest.firstChild.attributes['data-react-checksum']) {
    console.error('Server-side React render was discarded. Make sure that your initial render does not contain any client-side code.');
  }
}

That’s it!

Once you’ll try to run npm start and access localhost:3000, you’ll see the page rendered isomorphically:

8

We didn’t add meaningful routes yet, but if you’ll access http://localhost:3000/fhsfhslfhlks (non-existing uri, for example), you’ll see that NotFound route is also works, which means that router is working fine in isomorphic environment :)

If not, check this chapter’s code on a github: https://github.com/codingbox/isomorphic-react-redux-tutorial/tree/4a5da9ee3d5a7956e995f9acd318721db332ce4e

Improving development process

You might notice two uncomfortable things happening:

  • dev server is not reloading upon changes
  • lack of Redux DevTools

Let’s fix these problems.

The first one might be solved by using nodemon:

npm install nodemon --save-dev

We will need to modify package.json slightly after that, it should look like this afterwards:

  "scripts": {
    "dev": "concurrently --kill-others \"npm run dev-api\" \"npm run dev-server\" \"npm run webpack-dev-server\"",
    "dev-api": "NODE_ENV=development nodemon --exec babel-node ./bin/api.js",
    "dev-server": "NODE_ENV=development nodemon --exec babel-node ./bin/server.js",
    "webpack-dev-server": "NODE_ENV=development babel-node ./bin/webpack-dev-server.js",
    "start": "concurrently --kill-others \"npm run start-api\" \"npm run start-server\"",
    "start-api": "NODE_ENV=production babel-node ./bin/api.js",
    "start-server": "NODE_ENV=production babel-node ./bin/server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

After that,

npm run dev

Should be used for development and will give you nodemon information from now on in console:

9

While

npm run start

(without development reload features & other stuff) will be used for production environment. We’ll discuss production environment more in heroku (next) part.

Now, speaking about Redux DevTools.

They shouldn’t be running on server, so let’s add a specific flags in ./webpack/dev.config.js to distinguish browser and node env, I prefer to do it this way:

new webpack.DefinePlugin({
  __CLIENT__: true,
  __DEVTOOLS__: true,
  'process.env': {
    NODE_ENV: '"development"',
  },
}),

So the whole plugin section will look like this:

plugins: [
  // hot reload
  new webpack.HotModuleReplacementPlugin(),
  new webpack.IgnorePlugin(/webpack-stats\.json$/),
  new webpack.DefinePlugin({
    __CLIENT__: true,
    __DEVTOOLS__: true,
    'process.env': {
      NODE_ENV: '"development"',
    },
  }),
],

Be aware that you need to set these globals in eslint as those are working as preprocessor constants, so global.CLIENT, for example, won’t work to access those on client-side, but will work fine on server-side (this is important for debugging):

module.exports = {
  extends: 'airbnb',
  globals: {
    __CLIENT__: true,
    __DEVTOOLS__: true,
  },
};

You’ll also need to define CLIENT somewhere in ./bin/server.js. The place after import is a good one:

global.__CLIENT__ = false; // eslint-disable-line

If you’ll forget to do it, you might get the error like this:

ReferenceError: __CLIENT__ is not defined
   at createStoreWithReducer (/Users/viatsko/projects/isomorphic-react-redux-tutorial/src/redux/createStore.js:14:49)
   at createStore (/Users/viatsko/projects/isomorphic-react-redux-tutorial/src/redux/createStore.js:39:10)
   at /Users/viatsko/projects/isomorphic-react-redux-tutorial/bin/server.js:56:17
   at Layer.handle [as handle_request] (/Users/viatsko/projects/isomorphic-react-redux-tutorial/node_modules/express/lib/router/layer.js:95:5)
   at trim_prefix (/Users/viatsko/projects/isomorphic-react-redux-tutorial/node_modules/express/lib/router/index.js:312:13)
   at /Users/viatsko/projects/isomorphic-react-redux-tutorial/node_modules/express/lib/router/index.js:280:7
   at Function.process_params (/Users/viatsko/projects/isomorphic-react-redux-tutorial/node_modules/express/lib/router/index.js:330:12)
   at next (/Users/viatsko/projects/isomorphic-react-redux-tutorial/node_modules/express/lib/router/index.js:271:10)
   at SendStream.error (/Users/viatsko/projects/isomorphic-react-redux-tutorial/node_modules/serve-static/index.js:121:7)
   at emitOne (events.js:96:13)

Now, let’s hack createStore.js:

We’ll need to replace:

const finalCreateStore = applyMiddleware(...middleware)(_createStore);
  let finalCreateStore;
  if (process.env.NODE_ENV === 'development' && global.__CLIENT__ && global.__DEVTOOLS__) {
    const { persistState } = require('redux-devtools');
    const DevTools = require('../containers/DevTools');
finalCreateStore = compose(
      applyMiddleware(...middleware),
      global.devToolsExtension ? global.devToolsExtension() : DevTools.instrument(),
      persistState(global.location.href.match(/[?&]debug_session=([^&]+)\b/))
    )(_createStore);
  } else {
    finalCreateStore = applyMiddleware(...middleware)(_createStore);
  }

Don’t miss the fact you need to import compose from redux now:

import { createStore as _createStore, applyMiddleware, compose } from 'redux';

As you see, this, in turn, requires DevTools container, as following:

/* eslint import/no-extraneous-dependencies: 0 */
import React from 'react';
import { createDevTools } from 'redux-devtools';
import LogMonitor from 'redux-devtools-log-monitor';
import DockMonitor from 'redux-devtools-dock-monitor';
module.exports = createDevTools(
  <DockMonitor
    toggleVisibilityKey="ctrl-H"
    changePositionKey="ctrl-Q"
  >
    <LogMonitor />
  </DockMonitor>
);

And, at last, we need to add it to the view:

./src/index.js

In the end of file, add:

if (__DEVTOOLS__ && !global.devToolsExtension) {
  const DevTools = require('./containers/DevTools'); // eslint-disable-line
  ReactDOM.render(
    <Provider store={store} key="provider">
      <div>
        {component}
        <DevTools />
      </div>
    </Provider>,
    dest
  );
}

Please note that we’re duplicating rendering code here, but that’s fine as the first code will ensure we’re rendering according to checksum, while Redux DevTools rendering differently than what we expect from SSR.

If you’ve done everything correctly, DevTools will be available after reloading the services:

10

Enjoy playing with DevTools!

If something’s broken, here’s the repository state by the end of this chapter https://github.com/codingbox/isomorphic-react-redux-tutorial/tree/f0988631eb03218d14b0f038d12f3d52a6b9642e

Production

In this chapter, I’ll show you how to prepare the project for production in deploy it using Heroku as an example.

Why Heroku? I find it very useful to have a service which provides for a very cheap price all-in-one service: load balancer, scalability, databases, monitoring etc

You might use Google Cloud/other cloud instead of even VPS/dedicated server with the help of PM2, for example, but I think that developer should spend more time solving product problems rather than hacking infrastructure (of course, sometimes you have to, but after all this becomes just wasted time rather than investment).

So!

Heroku requires to have Procfile for your app in order to understand how to start your application, let’s create a simple one:

web: npm run start

Now, let’s stop for a minute and think what we are going to run in production. Remember the beginning of the article? Yep, we’re not going to keep webpack-dev-server in production environment, we are going to prebuild the bundle.

Let’s create production webpack config:

./webpack/prod.config.js

/* eslint import/no-extraneous-dependencies: 0 */
const path = require('path');
const webpack = require('webpack');
const assetsPath = path.resolve(__dirname, '../public/assets');
module.exports = {
  devtool: 'source-map',
  context: path.resolve(__dirname, '..'),
  entry: {
    main: [
      './src/index.js',
    ],
  },
  output: {
    path: assetsPath,
    filename: '[name].js',
    chunkFilename: '[name]-[chunkhash].js',
    publicPath: '/assets/',
  },
  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        exclude: /(node_modules|bower_components)/,
        loader: 'babel',
      },
    ],
  },
  progress: true,
  resolve: {
    modulesDirectories: [
      'node_modules',
      'src',
    ],
    extensions: ['', '.json', '.js', '.jsx'],
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"',
      },
      __CLIENT__: true,
      __DEVTOOLS__: false,
    }),
    // ignore dev config
    new webpack.IgnorePlugin(/\.\/dev/, /\/config$/),
    // optimizations
    new webpack.optimize.DedupePlugin(),
    new webpack.optimize.OccurenceOrderPlugin(),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false,
      },
    }),
  ],
};

And let’s add build script to package.json, so we’ll be able to build webpack bundle using npm run build command:

"build": "webpack --verbose --colors --display-error-details --config webpack/prod.config.js",
"postinstall": "npm run build"

Time to create heroku app:

heroku apps:create 

11

In the end of build process Heroku should report you the resulting url of your app:

12

If, for some reason, app won’t be working, heroku provides an ability of looking at application logs using command:

heroku logs -t

That’s it!

The resulting application is living in this github repository: https://github.com/codingbox/isomorphic-react-redux-tutorial

And is running on Heroku at https://isomorphicreactredux.herokuapp.com/

Homework

I was not talking about css in this article. When you’ll start adding it to your app you might face the problem that, while you can have .css files in your client bundle, you cannot include css files server-side. To solve this problem, I’d recommend you to look into webpack-isomorphic-tools https://www.npmjs.com/package/webpack-isomorphic-tools

© 2017 Valerii Iatsko. Personal opinions.
EN | RU