Universal React with Rails: Part IV

react on railsAugust 04, 2015Dotby Alex Fedoseev

Making Universal Flux app

In the previous post we’ve built dummy app, which was universal, but there were no interactions with API, no state / dynamic data to deal with. To build complex apps we need a solid concept of application’s design. In React world such concept is Flux.

:before

The list of stuff you need to be familiar with to feel comfortable while reading this post:

Flux

First of all, Flux is not a library or framework, it’s just pattern, so you should take care about the tools to implement this pattern. Facebook just gave few example apps and dispatcher thing. And you’re on your own with everything else.

Main issue with vanilla flux is that it’s not universal out of the box. Its stores are singletons, and singleton == problem on the server. If you’ll try to use it as is, store’s data will be leaking between requests. It means that visitor can get the content of the store of previous visitor. Nice surprise, eh!

There are a lot of flux implementations that came out to solve this issue. And I’ve spend pretty much time exploring them. Eventually I choose Flummox — isomorphic-in-mind Flux library with very neat API. I rewrote demo app for this series and, before start this post, I decided to try it on bigger app. So I rewrote my blog. Right after publishing it Flummox was deprecated in favour of Redux — library written by Dan Abramov.

Redux

So what’s so special about Redux?

Stores in vanilla flux are the owners of the state (basically it means that they are containers for their data). One flux application can have multiple stores (like models in MVC). In Redux there are no separated stores, but there is only one global store with data tree, which holds the state of whole application.

But how to handle changes of the state? For that Redux introduced reducers — state’s transformation logic, extracted from vanilla stores to stand-alone essence. Reducer is a pure function. It takes current state and action as arguments and returns new state. Here is the signature:

function reducer(state, action) -> newState

And here is how it looks like:

image1

When user triggers action creator from component, it performs something (request to API for example) and dispatches action (as result) to reducer. Action is simple javascript object, usually looks like this:

const action = {

  type   : ACTION_TYPE,

  payload: someData

};

Reducer takes action, transforms (NOT mutates!) current state and returns new one. All components, subscribed to updates, receives new state within props and — UI updated!

At this point let’s get back to Isomorphic comments demo app. It already has JSON API and now we will build a client part.

Official Redux docs are still in progress, but you can check out significant part of them here.

Implementation

This part is gonna be hard. To make it easier to follow, here is the link to github repo with application.

Let’s list the features we have to implement:

  1. Create comment.
  2. List comments.
  3. Show comment.
  4. Destroy comment.

Bundle structure

Before we begin, let’s take a look what’s going on inside bundle folder.

|-- /app/bundles/[bundle]

|------ /actions

|------ /components

|------ /constants

|------ /decorators

|------ /initters

|------ /layouts

|------ /reducers

|------ /routes

I propose to start not from point of initialization of Redux, but from user action in Component (new comment submission) — and then to follow the data flow. Hope this way it will be easier to get the concept.

Create comment

First there was a form:

/* app/bundles/app/components/Comments/Form.jsx */

_handleSubmit() {
  
  // Validation stuff...

  // Collecting data
  const { author, comment } = this.state;
  const { commentsActions } = this.props;

  const data = {
    'comment': { author, comment }
  };

  // Triggering action creator with new comment
  commentsActions.addComment({ data });

}


render() {

  return (
    <form onSubmit={this._handleSubmit}>
      <input type="text" value={this.state.author} onChange={this._handleValueChange} />
      <input type="text" value={this.state.comment} onChange={this._handleValueChange} />
      <button>Comment!</button>
    </form>
  );

}

As you can see, when form is submitted, action creator is triggered. So next we’ll move to this action creator. But before that let’s define a few constants aka action types.

/* app/bundles/app/constants/CommentsConstants.js */

// Action type: new comment was POSTed to API (we're waitnig for response here)
export const COMMENT_ADD_REQUESTED = 'COMMENT_ADD_REQUESTED';

// Action type: new comment was successfully created
export const COMMENT_ADD_SUCCEED = 'COMMENT_ADD_SUCCEED';

// Action type: new comment was rejected by API
export const COMMENT_ADD_FAILED = 'COMMENT_ADD_FAILED';

If there wasn’t API call, action creator would look like this:

/* app/bundles/app/actions/CommentsActions.js */

import * as actionTypes   from '../constants/CommentsConstants';

export function addComment({ data }) {

  // Returning action object with action type and payload (comment)
  // It will be immediately dispatched to reducer
  return {
    type   : actionTypes.COMMENT_ADD_SUCCEED,
    comment: data
  };

}

But we should handle async stuff here:

/* app/bundles/app/actions/CommentsActions.js */

import apiCall            from 'app/libs/apiCall';

import * as actionTypes   from '../constants/CommentsConstants';


export function addComment({ data }) {
  
  // Returning a function!
  return dispatch => {

    // Dispatching action COMMENT_ADD_REQUESTED
    // It means: request to API sent, waiting for response
    dispatch({
      type: actionTypes.COMMENT_ADD_REQUESTED
    });

    // This is `apiCall` helper
    // Performs request, returns a Promise
    return apiCall({
      method: 'POST',
      path  : '/comments',
      data  : data
    })
      .then(res => {
        
        // Success!
        // Dispatching payload with created comment.
        dispatch({
          type   : actionTypes.COMMENT_ADD_SUCCEED,
          comment: res.data.comment
        });
      })
      .catch(res => {
        
        // Oops! Something went wrong.
        // Dispatching errors from API.
        dispatch({
          type  : actionTypes.COMMENT_ADD_FAILED,
          errors: {
            code: res.status,
            data: res.data
          }
        });
      });

  };

}

You can explore apiCall helper here (it uses axios lib by Matt Zabriskie).

Now, when all actions were dispatched, we’re moving to comments reducer:

/* app/bundles/app/reducers/comments.js */

import * as actionTypes  from '../constants/CommentsConstants';

// Initial state of comments branch of the store
const initialState = {
  type        : null,
  comments    : [],
  errors      : null,
  isPosting   : false
};

// Input: state, action
export default function comments(state = initialState, action) {

  // Action to variables
  const { type, comment, errors } = action;

  switch (type) {

    // If COMMENT_ADD_REQUESTED
    case actionTypes.COMMENT_ADD_REQUESTED:
      // Updating state -> showing loader in component
      return {
        ...state,
        type,
        isPosting: true
      };

    // If COMMENT_ADD_SUCCEED -> adding it to comments array
    case actionTypes.COMMENT_ADD_SUCCEED:

      // We should NEVER mutate state
      // In complex apps we should use immutable.ls
      // But here we're just creating a copy of `state.comments` array
      let withNewComment = state.comments.slice();
      
      // Unshifting new comment
      withNewComment.unshift(comment);

      // Updating state with new comment
      return {
        type,
        comments : withNewComment,
        errors   : null,
        isPosting: false
      };

    // If COMMENT_ADD_FAILED
    case actionTypes.COMMENT_ADD_FAILED:
      // Updating state with errors -> handling them in component
      return {
        ...state,
        type,
        errors,
        isPosting: false
      };


    default:
      return state;

  }

}

State updated! But how components will know that? To notify them about store updates, we should wrap them in apex component, which will be subscribed to state updates and will be passing new data from store to its children via props every time store gets changed.

Redux gives us two options for this:

  • Connector Higher-order Component

    Pros: ES standards compliance.

    Cons: verbose.

  • @connectdecorator

    Pros: much less verbose.

    Cons: Babel, stage one.

/* Higher-order Component: Connector */

/* app/bundles/app/components/Comments/CommentsContainer.jsx */

import React                    from 'react';
import { bindActionCreators }   from 'redux';
import { Connector }            from 'react-redux';

import Comments                 from './Comments';

import * as CommentsActions     from '../../actions/CommentsActions';


export default class CommentsContainer extends React.Component {


  constructor(props, context) {
    super(props, context);
  }


  render() {
    
    // Connector has prop `select`, it's a function ->
    // receiving state, returning selected branches of the state
    // Connector's child is a function ->
    // receiving state branches and dispatcher, returning UI Component with props:
    // state branches, action creators and the rest
    return (
      <Connector select={state => ({ comments: state.comments })}>
        {({ comments, dispatch }) =>
          <Comments 
              comments={comments} 
              commentsActions={bindActionCreators(CommentsActions, dispatch)}
              {...this.props}
          />
        }
      </Connector>
    );

  }

}
/* decorator: @connect */

/* app/bundles/app/components/Comments/CommentsContainer.jsx */

import React                    from 'react';
import { bindActionCreators }   from 'redux';
import { connect }              from 'react-redux';

import Comments                 from './Comments';

import * as CommentsActions     from '../../actions/CommentsActions';

// @connect decorator -> selecting branches of the state...
@connect(state => ({
  comments: state.comments
}))

export default class CommentsContainer extends React.Component {


  constructor(props, context) {
    super(props, context);
  }


  render() {

    // ... and passing them with dispatcher to Component via props
    const { comments, dispatch } = this.props;

    // returning Component with branches of the state, action creators and the rest
    return (
      <Comments
          comments={comments}
          commentsActions={bindActionCreators(CommentsActions, dispatch)}
          {...this.props}
      />
    );

  }

}

I like decorators and in this case my choice is @connect option.

More about @decorators in Addy Osmani’s post.

Thus our components are subscribed to state updates. Moving forward.

List comments

Now when we can create comments, we should learn how to properly list them. It would be trivial, if there weren’t one thing: we have server rendering. So we should deal with data fetching not only on the client, but on the server too. And this is kinda tricky.

Let’s pretend we don’t have server. How should we handle data fetching on the client? In this case we can mount component and make API call from componentDidMount() method. While data is loading, user will be seeing a loader. When it’s done — the data are shown.

But on the server, when we render with renderToString() method, componentDidMount() is out, because it’s triggered only by render() method on the client. So we need to fetch the data and put it to global store before we’ll call renderToString(). This way, when React will start rendering, store will be already filled up, so components will get the data from store via props => user will get initial html with data inside. So we need a plan how to achieve this.

The plan. In every apex component will be defined static method, which calls action creator, responsible for data loading. This method will be triggered:

  • On server — to pre-fetch the data before rendering initial html.
  • On client — from componentDidMount() every time when component is mounted.

One more thing to keep in mind: we shouldn’t call this method on the client after very first render — when initial data was already loaded on server and restored (dehydrated) on the client.

The realisation. Component (route handler) for every route, where data fetching is required — is apex component:

import React      from 'react';
import { Route }  from 'react-router';

import App        from '../layouts/App';
import NotFound   from '../components/NotFound/NotFound';

// This apex component is subscribed to store updates
// And has static method `fetchData`
import Comments   from '../components/Comments/CommentsContainer';


export default (

  <Route name="app" component={App}>

    <Route name="comments"     path="/"                component={Comments} />
    <Route name="comment"      path="/comments/:id"    component={Comments} />

    <Route name="not-found"    path="*"         component={NotFound} />

  </Route>

);

Thus these apex components will be included in initialState.components array, provided by react-router. On the server we’ll iterate this array and call static fetchData methods of apex components. To handle asynchronicity we’ll use experimental ES7 feature async/await:

/* app/libs/initters/server.js */

// Initializing Redux (details later)...

// Running router
// Here we're using experimental feature `async/await`
Router.run(routes, location, async (error, initialState, transition) => {

  try {

    // The Promise.all(iterable) method returns a promise 
    // that resolves when all of the promises in the iterable argument have resolved.

    // Awaiting until all Promises will be resolved
    await Promise.all(
      
      // Array of apex components from routes.jsx
      initialState.components
          // We need only components with `fetchData` method
          .filter(component => component.fetchData)
          // Fetching the data!
          // `args` is object with required arguments for `fetchData` method
          .map(component => component.fetchData({ /* args */ }))
    );
    
    // At this point we have store full of data
    // So we're rendering stuff, sending response...
    
    // Complete initter will be shown in the end of the post
    
  }

}

To put these statics on components we’ll use one more decorator @fetchData:

/* app/bundles/app/components/Comments/CommentsContainer.jsx */

import React                    from 'react';
import { bindActionCreators }   from 'redux';
import { connect }              from 'react-redux';

import Comments                 from './Comments';

import fetchData                from '../../decorators/fetchData';

import * as CommentsActions     from '../../actions/CommentsActions';


// @fetchData decorator takes function as argument
// This function triggers action creator, responsible for data loading
// For server side request we have to pass apiHost (server initter will grant it)
@fetchData(({ apiHost, dispatch }) => {
  return dispatch(CommentsActions.loadComments({ apiHost }));
})

@connect(state => ({
  comments: state.comments
}))

export default class CommentsContainer extends React.Component { /* ... */ }

And finally — @fetchData decorator:

/* app/bundles/app/decorators/fetchData.jsx */

import React  from 'react';

// `fetch` function as argument
export default fetch => {

  // Returning function: 
  // receiving `DecoratedComponent`, 
  // returning class `FetchDataDecorator` (kinda HOC)
  return DecoratedComponent => (

    class FetchDataDecorator extends React.Component {

      // Static `fetchData` (required for server-side data fetching)
      static fetchData = fetch

      // Required for data fetching on the client
      componentDidMount() {

        // If it's very first render -> do not fetch, all done on server
        // We will pass this flag in client's initter
        if (this.props.initialRender) return;

        // Else -> fetching data
        const { location, params, store } = this.props;

        fetch({
          location,
          params,
          dispatch: store.dispatch
        });

      }


      render() {

        // Rendering DecoratedComponent
        return (
          <DecoratedComponent {...this.props} />
        );

      }

    }

  );

}

Thanks to Quangbuu Le for this trick. Now we have one method, which we can use on the server and on the client to fetch the data from API.

Redux initialization

Oops, almost forgot about this one (: To make all this stuff works, we need Redux to be initialized. In server’s initter we’re creating the store, then populating it with data, rendering head and body, serializing global state to expose it as global variable on the client, thus we can restore state from the server on the client (to avoid duplicate request to API) and finally compiling Jade template with all the stuff.

Server initter:

/* app/libs/initters/server.jsx */

import React                from 'react';
import Router               from 'react-router';
import Location             from 'react-router/lib/Location';
import { combineReducers }  from 'redux';
import { applyMiddleware }  from 'redux';
import { createStore }      from 'redux';
import { Provider }         from 'react-redux';
import middleware           from 'redux-thunk';
import serialize            from 'serialize-javascript';
import jade                 from 'jade';


export default async (req, res, next, params) => {

  // Combining reducers into one parent reducer
  const reducer = combineReducers(params.reducers);
  
  // Creating store:
  // 1. Applying redux-thunk middleware to perform async actions
  //    (we can write and apply our own, there can be multiple middlewares here)
  // 2. Creating store without reducers
  // 3. Adding reducers
  const store = applyMiddleware(middleware)(createStore)(reducer);

  // Location for router
  const location = new Location(req.path, req.query);

  // Hosts for api calls and <head> section rendering
  const appHost = `${req.protocol}://${req.headers.host}`;
  const apiHost = `${req.protocol}://api.${req.headers.host}`;

  const { routes } = params;

  Router.run(routes, location, async (error, initialState, transition) => {

    try {

      // Fetching data
      await Promise.all(
        initialState.components
            .filter(component => component.fetchData)
            .map(component => component.fetchData({ apiHost, dispatch: store.dispatch }))
      );
    
      // Getting the state
      const state = store.getState();

      let { bundle, locals } = params;

      // Rendering <head>
      locals.head = React.renderToStaticMarkup(
        React.createElement(params.Head, { /* args */ })
      );

      // Rendering body, notice Provider component
      locals.body = React.renderToString(
        <Provider store={store}>
          {() => <Router location={location} {...initialState} />}
        </Provider>
      );

      const chunks = __DEV__ ? {} : require('public/assets/chunk-manifest.json');

      locals.chunks = serialize(chunks);
      
      // Serializing state to expose it on client via global variable
      locals.data = serialize(state);

      // Compiling Jade template
      const layout = `${process.cwd()}/app/bundles/${bundle}/layouts/Layout.jade`;
      const html   = jade.compileFile(layout, { pretty: false })(locals);

      // 😽💨
      res.send(html);

    } catch (err) {

      res.status(500).send(err.stack);

    }

  });

}

Jade template:

doctype html
html
  != head

  body
    #app!= body
    
    script.
      window.__CHUNKS__ = !{chunks};
    
    // Serialized state
    script.
      window.__DATA__ = !{data};
    
    script(src="#{vendorAsset}")
    script(src="#{jsAsset}")

Client initter:

/* app/libs/initters/client.jsx */

import React                from 'react';
import Router               from 'react-router';
import BrowserHistory       from 'react-router/lib/BrowserHistory';
import { combineReducers }  from 'redux';
import { applyMiddleware }  from 'redux';
import { createStore }      from 'redux';
import { Provider }         from 'react-redux';
import middleware           from 'redux-thunk';


export default (params) => {

  // Same as on the server, but on the client we provide initial state, 
  // passing `window.__DATA__` as a second argument
  const reducer = combineReducers(params.reducers);
  const store = applyMiddleware(middleware)(createStore)(reducer, window.__DATA__);

  // History for the router
  const history = new BrowserHistory();

  const { routes } = params;
  

  // This is initial rendering
  let initialRender = true;

  // React-router 1.0.0 gives the ability 
  // to customize Component's rendering 
  // passing custom function to `Router`s `createElement` prop 
  // (see `AppContainer` const below)
  const appComponent = (Component, props) => {
    
    // Passing `initialRender` flag as prop, required for @fetchData decorator
    return (
      <Component initialRender={initialRender} {...props} />
    );

  };

  // Creating app container
  const AppContainer = (
    <Provider store={store}>
      {() => <Router history={history} children={routes} createElement={appComponent} />}
    </Provider>
  );

  // Selecting DOM container for app
  const appDOMNode = document.getElementById('app');

  // When app is flushed to the DOM -> setting `initialRender` to `false`
  React.render(AppContainer, appDOMNode, () => initialRender = false);

}

Whew! Few years ago I thought my Excel reports were tricky.

You can explore how to show and delete comments in github repo, pattern is the same.

Conclusion

Ok, now we have Universal Flux / Redux app, which is already pretty cool, but still un-secure and runs only on local machine. Next time we will add simple authentication mechanism, and after that we’ll deploy everything to production server.

Stay tuned!

Part I: Planning the application

Part II: Building JSON API

Part III: Building Universal app

Part IV: Making Universal Flux app

Closing Remark

Could your team use some help with topics like this and others covered by ShakaCode's blog and open source? We specialize in optimizing Rails applications, especially those with advanced JavaScript frontends, like React. We can also help you optimize your CI processes with lower costs and faster, more reliable tests. Scraping web data and lowering infrastructure costs are two other areas of specialization. Feel free to reach out to ShakaCode's CEO, Justin Gordon, at justin@shakacode.com or schedule an appointment to discuss how ShakaCode can help your project!
Are you looking for a software development partner who can
develop modern, high-performance web apps and sites?
See what we've doneArrow right