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
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 requestapp.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 statelet 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 storeconst finalState = store.getState();
// Send the rendered page back to the clientres.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-sidefunction 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-considerationswindow.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState)}</script><script src="/bundle.js"></script></body></html>`;}
// listen in the port 9800app.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 requestapp.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 storeconst finalState = store.getState();
// Send the rendered page back to the clientres.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-considerationswindow.__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 HTMLconst 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 libraryconst browserHistory = createBrowserHistory();
// Grab the state from a global variable injected into the server-generated HTMLconst 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 theInferno.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 pageimport 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.jsimport {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.jsclass 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 pageimport Inferno from 'inferno';import Component from 'inferno-component';
class App extends Component {render() {return(<div>{ this.props.children }</div>);}}
export default App;
// DiscPage.jsimport 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 Reactconst connectedStateAndProps = connect(mapStateToProps);
export default connectedStateAndProps(DiscPage);
// DiscComponent.js - just like in Reactclass 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
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!):