Mauricio M. Ribeiro

@maumribeiro

Server-side rendering and Redux: from React to Inferno

React has been a major actor in the new Javascript trending. Its open-source component-based environment made web development far easier and pleasuring.

With its concept, we are able to create powerful web apps running on the old buddy JS.

However, it came with some downsides, and one of them relates to the size of the applications (the library itself weighs 45 kb). A new trending of developing new lightweight libraries has come, and with them Inferno: https://www.infernojs.org/

And what’s cool: Inferno is React-like. So, if you love working with React, Inferno will definitely not be a big issue for you.

In this page I will show this, with a small sample of server-side rendering written in React; and the very same sample written in Inferno.

I’m also using Redux, even if I don’t really need it here, I used it just for fun :)

The code consists of a dummy page which displays a list of music albums retrieved from a backend API. The complete code (in React and Inferno), as well as the backend server, can be found here: https://github.com/morris-ribs/server-side-rendering

Let us cover three parts: the server rendering, the entrypoint, and the components.

The code is based in http://redux.js.org/docs/recipes/ServerRendering.html

The server rendering

In the entrypoint, we add the code that renders the app in server side and send it to the client.

Here is the code with React:

// server.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import {Provider} from 'react-redux';
import { match, RouterContext } from 'react-router';

import express from 'express';
import routes from '../src/routes';
import configureStore from '../src/store/configureStore';
import {getDataSuccess} from '../src/actions/discActions';
import {fetchData} from '../src/api/DiscApiClient';
 ...
const port = 9800;
const app = express();
// This is fired every time the server side receives a request
app.use(handleRender);
function handleRender(req, res) {
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message);
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search);
} else if (renderProps) {
fetchData().then(discs => {
// Compile an initial state
let preloadedState = {};

// Create a new Redux store instance
const store = configureStore(preloadedState);
store.dispatch(getDataSuccess(discs));
// You can also check renderProps.components or renderProps.routes for
// your "not found" component or route respectively, and send a 404 as
// below, if you're using a catch-all route.
const html = renderToString(<Provider store={store}><RouterContext {...renderProps} /></Provider>);
// Grab the initial state from our Redux store
const finalState = store.getState();
// Send the rendered page back to the client
res.status(200).send(renderFullPage(html, finalState));
}).catch(error => {
res.status(500).send(error.message);
});
} else {
res.status(404).send('Not found');
}
});
}
// the contents to be rendered on server-side
function renderFullPage(html, preloadedState) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<title>Discs Test</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
</head>
<body>
<div id="app">${html}</div>
<script>
// WARNING: See the following for Security isues with this approach:
// http://redux.js.org/docs/recipes/ServerRendering.html#security-considerations
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState)}
</script>
<script src="/bundle.js"></script>
</body>
</html>
`;
}
// listen in the port 9800
app.listen(port, function(err) {
if (err) {
console.log(err);
} else {
open(`http://localhost:${port}`);
}
});

And now, the code in Inferno (using inferno-server, inferno-router and inferno-redux).

Here the main difference is that we call the match function, getting the renderProps object from it, and then use it in our RouterContext:

// server.js
import Inferno from 'inferno';
import { renderToString } from 'inferno-server';
import {Provider} from 'inferno-redux';
import { match, RouterContext } from 'inferno-router';

import routes from '../src/routes';
import configureStore from '../src/store/configureStore';
import {getDataSuccess} from '../src/actions/discActions';
import {fetchDara} from '../src/api/DiscApiClient';
const port = 9800;
const app = express();
// This is fired every time the server side receives a request
app.use(handleRender);
function handleRender(req, res) {
const renderProps = match(routes, req.originalUrl);
if (renderProps.redirect) {
res.redirect(renderProps.redirect);
} else if (renderProps) {

fetchData().then(discs => {
// Compile an initial state
let preloadedState = {};

// Create a new Redux store instance
const store = configureStore(preloadedState);
store.dispatch(getDataSuccess(discs));

// You can also check renderProps.components or renderProps.routes for
// your "not found" component or route respectively, and send a 404 as
// below, if you're using a catch-all route.
const html = renderToString(<Provider store={store}><RouterContext {...renderProps} /></Provider>);
// Grab the initial state from our Redux store
const finalState = store.getState();
// Send the rendered page back to the client
res.status(200).send(renderFullPage(html, finalState));
}).catch(error => {
res.status(500).send(error.message);
});
} else {
res.status(404).send('Not found');
}
}
// Exactly like in React! 
function renderFullPage(html, preloadedState) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<title>Discs Test</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
</head>
<body>
<div id="app">${html}</div>
<script>
// WARNING: See the following for Security isues with this approach:
// http://redux.js.org/docs/recipes/ServerRendering.html#security-considerations
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState)}
</script>
<script src="/bundle.js"></script>
</body>
</html>
`;
}
// Exactly like in React!
app.listen(port, function(err) {
if (err) {
console.log(err);
} else {
open(`http://localhost:${port}`);
}
});

The entrypoint

Here is where we are going to setup Redux store and provider

In React, it is like this:

// src/index.js
import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import {Router, browserHistory} from 'react-router';

import configureStore from './store/configureStore';
import routes from './routes';
import {loadData} from './actions/discActions';
// Grab the state from a global variable injected into the server-generated HTML
const preloadedState = window.__PRELOADED_STATE__;
const store = configureStore(preloadedState);
store.dispatch(loadDiscs());
render(
<Provider store={store}>
<Router history={browserHistory} routes={routes} />
</Provider>
,
document.getElementById('app')
);

In Inferno, we will make some changes, such as passing the main component in the router:

// src/index.js
import Inferno from 'inferno';
import configureStore from './store/configureStore';
import { createBrowserHistory } from 'history';
import {Provider} from 'inferno-redux';
import {Router, Route, IndexRoute} from 'inferno-router';

import {loadData} from './actions/discActions';
import App from './components/App';
import DiscPage from './components/disc/DiscPage';
// in Inferno, we need to create the browser history from another library
const browserHistory = createBrowserHistory();
// Grab the state from a global variable injected into the server-generated HTML
const preloadedState = window.__PRELOADED_STATE__;
const store = configureStore(preloadedState);
store.dispatch(loadData());
// the main App component goes as a param to Router
// The component page goes into the
Inferno.render(
<Provider store={store}>
<Router history={browserHistory} component={ App }>
<IndexRoute component={DiscPage} />
</Router>
</Provider>
,
document.getElementById('app')
);

The album display component

Finally, the component which represents the display of the list of albums on our page.

In React I wrote it like:

// App.js
// This component handles the App template used on every page
import React, {PropTypes} from 'react';
class App extends React.Component {
render() {
return(
<div>
{this.props.children}
</div>
);
}
}
App.propTypes = {
children: PropTypes.object.isRequired
};
export default App;
// DiscPage.js
import {connect} from 'react-redux';
import Disc from './DiscComponent';
/* eslint-disable no-console */
class DiscPage extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
discs: Object.assign({}, this.props.discs)
};
}

render() {
const discsToDisplay = (this.props.discs.albums) ? this.props.discs.albums : [];
return (
<div>
<Disc discs={discsToDisplay} />
</div>
);
}
}
DiscPage.propTypes = {
discs: PropTypes.object.isRequired
};
function mapStateToProps(state) {
return {
discs: state.discs
};
}
const connectedStateAndProps = connect(mapStateToProps);
export default connectedStateAndProps(DiscPage);
// DiscComponent.js
class DiscComponent extends React.Component {
constructor(props, context) {
super(props, context);
}
render() {
return (
<div>
<ul>
{this.props.discs.map(disc =>
<li key={disc.id}>{disc.title} - {disc.artist} ({disc.year})</li>
)}
</ul>
</div>
);
}
}
DiscComponent.propTypes = {
discs: PropTypes.array.isRequired
};
export default DiscComponent;

Here in Inferno. You can see that we don’t need to declare the Prop Types of the component.

In addition to that, the Component class come in another library, inferno-component:

// App.js
// This component handles the App template used on every page
import Inferno from 'inferno';
import Component from 'inferno-component';
class App extends Component {
render() {
return(
<div>
{ this.props.children }
</div>
);
}
}
export default App;
// DiscPage.js
import Disc from './DiscComponent';
import {connect} from 'inferno-redux';
class DiscPage extends Component {
render() {
const discsToDisplay = (this.props.discs.albums) ? this.props.discs.albums : [];
return (
<div>
<Disc discs={discsToDisplay} />
</div>
);
}
}
function mapStateToProps(state) {
return {
discs: state.discs
};
}
// with inferno-redux, we can connect components to the immutable store just like in React
const connectedStateAndProps = connect(mapStateToProps);
export default connectedStateAndProps(DiscPage);
// DiscComponent.js - just like in React
class DiscComponent extends Component {
constructor(props, context) {
super(props, context);
}
render() {        
return (
<div>
<ul>
{this.props.discs.map(disc =>
<li key={disc.id}>{disc.title} - {disc.artist} ({disc.year})</li>
)}
</ul>
</div>
);
}
}
export default DiscComponent;

And now we compare the size

As you can see, writing an app using server-side rendering and Redux is not that different from React and Inferno.

So, let’s see if there is really a difference regarding the sizes of the bundled JS between the two libraries, together with the corresponding ones for routing, redux and server-rendering. (Note: I’m using version 15.0.2 of React)

React (v 15.0.2): 308 Kb

Inferno (v 1.0): 170 Kb

Conclusion

Inferno brings a powerful and light library that is really friendly for developers who are used to work in React.

It brings all the best from React ecosystem, with the probability of using the most advanced features of it. It is really a good choice for web component-based apps in a light way (it weighs only 7 kb).

React team is already working hard to leverage the size and improve performance of the library. When it comes, it is really worth it to take a look on it.

Interesting links on both libraries (and more!):

  1. React
  2. Inferno
  3. Introduction To Functional Front-Ends With Inferno — Side Effects And Routing
  4. React and Redux Single Page Applications Resources

More by Mauricio M. Ribeiro

Topics of interest

More Related Stories