Universal React with Rails: Part IV
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. Official. Recommended (if you’ve never heard about Flux).
- Everything from the same list in previous 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:
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:
- Create comment.
- List comments.
- Show comment.
- 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.
-
@connect — decorator
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 III: Building Universal app
Part IV: Making Universal Flux app