Isomorphic React Redux example

Gethyl George Kurian
11 min readDec 20, 2016

--

This will help anyone who is trying to learn how to build a complete Isomorphic React-Redux app.

Well you probably already know how TO-DO list has become like the Hello World program when it comes to learning or testing a new Javascript framework. And due to this, I will use To-Do list to show how Isomorphic React-Redux works.

Before we move forward, if you want to understand what isomorphic app is, what are its advantages and why it is trending now, you can read this link.

You can refer to the full code in GitHub.

This is how our page would eventually will look.

Home page for To-do list

As you may have already noticed I am using bootstrap to get the styling and navigation in place.

What all can you do in this to-do app?

  • You can add a new item.
  • Edit an item to change the task name.
  • Delete an item.
  • And finally, mark an item as complete when you are done with the task.

Where do I start from?

You probably might be thinking from where do you start an isomorphic app. Should you start designing and coding from the server end or the client end?

Well my suggestion would be to create a normal client based app as you would normally do and then we just do some final tweaks to plug into the server and serve it from there.

Basic folder structure

Below is the folder structure that I have used.

dist — Will be the destination library from which we refer the CSS and the bundled JS

node_modules — All the node modules that we install with npm along with the dependencies will be present here. This folder is automatically created.

src — The source folder. This is where all the action happens

.babelrc — Configurations for Babel. eg. presets or plugins

index.html — Will be used for client side rendering.

package.json — npm dependency packages and scripts.

webpack.config.js —Basic configuration used by webpack

Under src, you can see the subfolders css, js and views

  • css — Nothing much happening here. Just some basic styling rest is covered by Bootstrap.
  • js — We further divide it into three subfolders based on where its intended to be used.
  • views — We will use ejs for templating the HTML. This is not required when you are using just client, and we use index.html for client, but when it comes to initial rendering from server, I will explain why we use it.

Looking deeper into the folder structure under src/js

  • client — Has client.js which is the entry point when we run the app as client only
  • server — Has server.js, which will be the entry point when we run the app as Isomorphic.
  • shared — This contains the React Components and Redux Actions and Reducers. The react components are not further split into smart and dumb components here. But if you are interested to know which are smart components, they are the once with connect decorator.

@connect()

Let us get a started now shall we?

To start with you need to install all the dependencies.

npm i -S <package-name>

You can refer to the below package.json, to find all the packages required and install the right away. This includes the packages that will be used for both Client and Server. Or you can just install them as and when you think you would need to use it.

Building the client app

If you have already created a client app and are looking to converting it to isomorphic, skip this section and jump to the last section.

Let us first start by creating the client side only React-Redux to-do list.

If you are not familiar with React or Redux, I would recommend you to click on the respective links to learn further before you proceed.

  1. As mentioned earlier, the entry point is from client.js
const app = document.getElementById('app');const initialState = window.__REDUX_STATE__
const store = createStore(allReducer,initialState);
ReactDOM.render(
<Provider store={store}>
<Router history={browserHistory} routes={routes} />
</Provider>
, app);

Ignore initialState for now, this is something which is used to make the app isomorphic. You could start with just creating the store as shown below if you want.

const store = createStore(allReducer);

As you see, the client.js setups up some very important things.

  • It creates the Redux store.
  • And it passes it to React, making it available to our smart components. This is done through react-redux npm package.
<Provider store={store}>
  • Also as you see we set up the routing for our single page application (SPA). This is done by react-router npm package.
<Router history={browserHistory} routes={routes} />

routes prop that you see above is the routes we import from routes.js

Now that you are clear with the entry point. Lets see what happens next by looking at the route. When we land on the default path /

React will render Layout and Index component as it is the IndexRoute.

And Index component will render ToDoList component.

/help contains a dummy page and this was created just to show the navigation using bootstrap. We will not look further into this.

2. Now let us look further into the TodoList.js

return(
<div>
<AddTodo/> <ListToDoItems/> </div>
);

It will render AddTodo and ListToDoItems

Before we go further into the above mentioned components, I feel this would be the right place to introduce you to the redux actions and reducers I have used.

3. ToDoActions, ToDoReducers and AllReducers

  • When ever react has to update the state, it has to do it via the store of redux. For this our react component will have to dispatch actions which is present inToDoActions.js

An important point to note here is. Action should always have a property Type. Rest of the properties can be anything based on what data you wish to pass

  • Our reducer will pick up the the action and create a new state object. All the reducers related to to-do can be found in ToDoReducers.js
const initialState = { items:[]}
let id = 0;
const todoReducer = (state=initialState, action) => {
switch (action.type) {
case 'ADD_ITEM':
return {
...state,
items: state.items.concat({id:id++,text:action.item,editItem:false,completed:false})
}
......
....
...

1. A very important point here is, state should never be mutated. i.e our state should always be immutable.

And this why you can see in our reducer we use the spread operator (…) to create a new state object each time and then update the relevant information in that. Spread operator is part of ES7.

2. Also notice that we pass an initial state to the Reducer? In our case we need our initial state to have an items array which is empty.

  • If we have multiple Reducer each catering to different functionalities or parts of the app, then it is better to keep them in separate files and combine them using combineReducers as shown in AllReducers.js

Below you can find all the js files related to actions and reducers for the to-do list app.

If you have seen the above snippets, and have a good understanding on how the actions and reducers look, then let us get back to our components and try dispatching some actions.

4. AddTodo component

AddTodo component highlighted in RED

AddTodo component code:-

As you see above, when the user enters something on the input field and when he clicks the Add Item button, we dispatch the AddItem action with the new to-do item.

this.props.dispatch(AddItem(newItem))

The action gets fired which in turn is picked by the reducer and updates the state in the Redux store by adding a new item into the items array

So now you might be wondering how will this update my ListToDoItems component and list out the new item?

Well it’s simple, ListToDoItems is also a smart component so it is listening to our store for any changes. So when AddToDo updates the state, ListToDoItems fetches the latest state from the store.

4. ListToDoItems component

ListToDoItems renders the section marked in GREEN

This component will list the items that are added and also dispatch actions when we try to edit an item, delete or mark it as complete. In the below gist, you can see how these three actions are dispatched.

How to run the client code which is created?

This is where you will need webpack and webpack-dev-server

Webpack is a module bundler which we will use to bundle our js files and place it in dist/js folder.

If you check webpack.config.js you can see that for bundling, we mention the entry point and the output as below:-

module.exports = {
entry: path.join(__dirname, 'src/js/client', 'client.js'),
output: {
path: path.join(__dirname, 'dist', 'js'),
filename: 'bundled.js'
},
debug:true,
devtool: 'source-map',
module: {
loaders: [{
test: path.join(__dirname, 'src/js'),
loader: ['babel-loader'],
query: {
//cacheDirectory: 'babel_cache',
presets: ['react', 'es2015','stage-0'],
plugins: ['react-html-attrs', 'transform-class-properties', 'transform-decorators-legacy'],
}
},
{ test: /\.css$/, loader: "style-loader!css-loader" },
{ test: /\.png$/, loader: "url-loader?limit=100000" },
{ test: /\.jpg$/, loader: "file-loader" },
{
test: /\.woff($|\?)|\.woff2($|\?)|\.ttf($|\?)|\.eot($|\?)|\.svg($|\?)/,
loader: 'url-loader'
}
]
},

Also few things that you can observe is, we use babel-loader to build our js files and transpile ES6 to ES5 while bundling. The presets and plugins mentioned in the config file are important to transpile correctly from ES6 to ES5 and to understand decorators etc.

If you notice the script in package.json, we have

“scripts”: {
“dev”: “webpack-dev-server — content-base — inline — hot”,

we use webpack-dev-server for client and therefore you can run

npm run dev

If your build is successful, go to http://localhost:8080/ to see client to-do app in action :)

Converting it into Isomorphic

If you have reached here, I believe your client app is running perfectly as expected?

Great, let us now move to next step of making it isomorphic.

To start with, we need to create a simple Express server.

If you haven’t already installed express, ejs, babel-cli, path earlier via npm, then please install them using npm i -S <package-name> before we move further.

Once all the dependencies are installed, create a file called server.js

And add the below code

var path = require('path');
var express = require('express');
var app = express();// start the server
var port = process.env.PORT || 4000;
var env = process.env.NODE_ENV || 'development';
app.listen(port, (err) => {
if (err) {
return console.error(err);
}
console.info('+++Server running on http://localhost:' + port + ' [' + env + ']');
});

And test from node using the below command:

babel-node src/js/server/server.js

and go to http://localhost:4000/ and see if the server is firing up.

Great! you have a simple express server running. Now let us add the react and redux related code into server.js and fire it up again.

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../../views'));
app.use(express.static(path.join(__dirname, '../../../dist')));app.get('*', (req, res) => {match({ routes: routes, location: req.url }, (err, redirectLocation, renderProps) => {// in case of error display the error message
if (err) {
return res.status(500).send(err.message);
}
// in case of redirect propagate the redirect to the browser
if (redirectLocation) {
return res.redirect(302, redirectLocation.pathname + redirectLocation.search);
}
var markup,
store,
initialState = {todoReducer:
{
items: [{id:0,text:"Initial State To do Item",editItem:false,completed:false}]
}
}

store = createStore(allReducers,initialState)
initialState = store.getState() //JSON.stringify(store.getState())
if (renderProps) {
markup = renderToString(
<Provider store={store}>
{ <RouterContext {...renderProps} />}
</Provider>
)
}
return res.render('index', { markup: markup, initialState: initialState });
});
});

Now add the above code right after var app = express();

We will try to understand the above code before proceeding further.

  • First we mention the template engine that we are going to use and from where it has to be picked. In our case it is index.ejs from views folder
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../../views'));
  • Next we mention from where the server has to pick the static files.

app.use(express.static(path.join(__dirname, ‘../../../dist’)));

At this point if you fire the server again you will see a message Cannot GET /

This is because we are not sending anything back to the client from the server.

Next we will add the remaining part of the code.

app.get('*',  (req, res) =>  {match({ routes: routes, location: req.url },  (err, redirectLocation, renderProps) => {// in case of error display the error message
if (err) {
return res.status(500).send(err.message);
}
// in case of redirect propagate the redirect to the browser
if (redirectLocation) {
return res.redirect(302, redirectLocation.pathname + redirectLocation.search);
}
var markup,
store,
initialState = {todoReducer:
{
items: [{id:0,text:"Initial State To do Item",editItem:false,completed:false}]
}
}

store = createStore(allReducers,initialState)
initialState = store.getState() //JSON.stringify(store.getState())
if (renderProps) {
markup = renderToString(
<Provider store={store}>
{ <RouterContext {...renderProps} />}
</Provider>
)
}
return res.render('index', { markup: markup, initialState: initialState });
});
});

What happens here is, we tell the server when the server receives a GET request, we need to match the request.url with that of from routes.js.

For this we will use the match function from react-router and if it matches, we create the initial state and the store using the initial state and the reducer.

initialState = {todoReducer:
{
items: [{id:0,text:"Initial State To do Item",editItem:false,completed:false}]
}
}

store = createStore(allReducers,initialState)

We are creating a dummy initial state with a to-do item called “Initial State To do Item”.

We pass this initial state along with the reducer which we created AllReducer to create the store.

Next we need to fill the markup which our index.ejs file is expecting.

<div id="app">
<%- markup -%>
</div>

For this we will create in server.js as follows

if (renderProps) {
markup = renderToString(
<Provider store={store}>
{ <RouterContext {...renderProps} />}
</Provider>
)
}

renderToString is used to render a React element to its initial HTML.

If you call ReactDOM.render() on a node that already has this server-rendered markup, React will preserve it and only attach event handlers, allowing you to have a very performant first-load experience.

If you have noticed our markup, it is similar to what we created in client.js but we use <RouterContext> here in server instead of <Router>. RouterContext will preserve the history, so you don’t need to add that as you do in Router.

Finally, we pass the markup, initial state along with our index.ejs as response to the request received.

return res.render(‘index’, { markup: markup, initialState: initialState });

So finally we are done with creating our Isomorphic app.

What are you waiting for? Fire up the server again.

I have added this script in package.json

“server”: “webpack && babel-node src/js/server/server.js”

This will build and bundle our files as well as start the server.

An advice here,

You should not be using babel-node in production. It is unnecessarily heavy, with high memory usage due to the cache being stored in memory. You will also always experience a startup performance penalty as the entire app needs to be compiled on the fly.

Simply run npm run server to fire up the server and watch your first isomorphic app.

When you go to http://localhost:4000/ you should see this:-

But are you seeing the “Initial State To do Item” ?

Well, you aren’t seeing it because we still have one tiny thing left to do. i.e we need to push our Initial state into a window object so that it could be used by the client as well.

Add the below <script> block in your index.ejs file.

<script type="text/javascript" charset="utf-8">
window.__REDUX_STATE__ = JSON.parse('<%- JSON.stringify(initialState) %>');

</script>

This is the initialState that you pass in res.render in server.js

And modify in client.js to get the intial state and use that to create your store in client as well.

const initialState = window.__REDUX_STATE__
const store = createStore(allReducer,initialState);

Fire up the server one final time, and this time you should see the initial to-do item that we are passing from the server :)

To Conclude

I hope this helps you guys in building up an isomorphic app using React and Redux.

Happy Coding!!

--

--