Managing data flow on the client-side

October 21th, 2015 by Bram Devries

I like to think of client-side applications as empty shells that get populated with data and where the source of that data can be anything: an object in your javascript code, an HTTP request, a JSON file, …

Making sure this data flows correctly through your application is one hell of a job. As your application grows it becomes harder and harder to manage the different data sources and how they are used to display content on the screens of your users.

This blog post is filled with examples. The syntax used is ES7. You can use Babel to compile it.

State vs. Props

On the last couple of projects we’ve been using React to build interactive user interfaces. React components have the concept of props and state to respectively pass data down to child components or manage data internally. Here’s a simple component:

// index.js
import React, {Component} from 'react';
import {render} from 'react-dom';

class Counter extends Component {
  static defaultProps = {
    increase: 1
  }

  state = {
    count: 0
  }

  handleClick() {
    this.setState({
      count: this.state.count + this.props.increase,
    });
  }

  render() {
    return (
      <div>
        <p>The button has been clicked {this.state.count} times.</p>
        <button onClick={::this.handleClick}>+{this.props.increase}</button>
      </div>
    );
  }
}

// Usage
render(<Counter />, document.getElementById('app'));

Whenever we click the button it will increase the counter by 1. The Counter component has a count state which keeps a record of the amount of times the button has been clicked. This state is internal, if I add 2 Counter components on a page each counter will start at 0 and they will increase separately.

This Counter component also contains a prop called increase. This allows us to set different increment values for each instance.

<Counter increase={2} />

The easiest way to think of props vs state is that the former can be seen as the initial configuration of a component, while the later is used to mutate the component over time.

A prop can be anything, in our Counter component we have a <button> with an onClick prop which contains a reference to Counter.handleClick.

Flux

For small applications you can rely on props and state, but once those applications start getting complex you might feel the need for a more structured approach. This is where Flux comes in.

Flux is a pattern designed by Facebook, it favors a uni-directional data flow and one-way data bindings.

The flux mental model

With Flux all data of a particular domain is kept in a store. A store has to register itself with the dispatcher. When an action is triggered by the dispatcher a callback will be executed on the store. The store makes the view aware of the new data by using an onChange event. The view then updates it’s own internal state which causes a re-render of the child views.

There are a lot of implementations of this pattern. Our favorite is Redux, which isn’t really considered an implementation of the Flux pattern. It took the best parts of Flux and elm.

For instance: Redux does not have the concept of a dispatcher, instead it relies on pure functions which brings it closer to a functional programming style. A second important difference is that Redux assumes your data is immutable, meaning you are always returning a new object or array.

Redux still uses actions to pass data from your components to your store, but we still need a way to define the changes happening to our state. This is where the reducer comes into play.

A reducer is a function that receives the previous state and the triggered action, and returns the next state.

There are a couple of things to keep in mind when writing a reducer:

  • You should never mutate the arguments
  • Add new data through an API call.
  • Add a certain randomness (using Math.random() or Date.now())

The rule of thumb for a reducer should be Given the same arguments, it should calculate the next state and return it. No surprises

The following steps are very similar to a typical Flux implementation. The store updates a controller view which then propagates the new state to its child components.

Dumb vs. Smart components

Both Redux and Flux have the concept of dumb and smart components.

Smart components are wrappers around dumb components, they are also called controller views. These components are aware of Redux and subscribe to a store. A smart component generally does not output any DOM.

Dumb components are not aware of Redux at all. They receive their data trough props via smart components and do output DOM.

The creator of Redux wrote a very nice article about how he handles smart and dumb components.

If you know something about design patterns this might sound familiar to you. Flux shares similarities with the commander pattern. Each action can be considered as a command and a reducer is the command handler.

In practice

Let’s rewrite our earlier counter example to a Redux application step by step.

Setup

Redux requires some boilerplate setup:

npm install --save redux react-redux redux-thunk

The first thing we will do is create our reducer:

// Reducers/Counter.js
export default function counter(state = 0, action) {
  return state;
}

Our initial reducer is very simple. It’s just a function that receives a state and action. For now we will let the reducer return the state. We also define a default state of 0.

// Reducers/index.js
import {combineReducers} from 'redux';
import counter from './Counter';

export default combineReducers({
  counter
});

This is some boilerplate Redux code. Typically each store will have its own reducers, but do note that an action will get passed down to every reducer. This allows you to handle a certain action differently. A good example would be a search component. You can manage the filtering of the results based on the query in one reducer, and update the search input in another.

The next step is creating our store:

// Stores/index.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducers from '../Reducers';

const createStoreWithMiddleware = applyMiddleware(
  thunk
)(createStore);

export default function store(initialState) {
  return createStoreWithMiddleware(reducers, initialState);
}

Again, this is some boilerplate code. In this step we’re essentially linking our store with our reducer.

Middlewares can be used for logging. By adding a logging middleware to all reducers, one can log all your application’s actions. Another example is a middleware for error reporting.

We’re also adding the redux-thunk middleware. This package allows you to execute async actions. If for some reason we decide to sync our counter to the server over an API we will need to make use of this.

A more advanced use-case of async actions is to dispatch other actions in another one. For example, when performing an API request our UI needs to be updated with a loading indicator before the request is started.

We also need to create our controller view:

// Containers/App.js
import React, {Component} from 'react';
import Counter from '../components/Counter';

export default class App extends Component {
  render() {
    return (
      <Counter />
    );
  }
}

And then finally we modify our bootstrap file so that it renders the App top level component instead of the Counter component.

// index.js
import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import createStore from './Stores';
import App from './Containers/App';

const store = createStore();

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('app'));

Our structure after setup looks like this:

├── Components
│   └── Counter.js
├── Containers
│   └── App.js
├── Reducers
│   ├── Counter.js
│   └── index.js
├── Stores
│   └── index.js
└── index.js

Connecting to a store

You might be wondering how we can connect our App top level component to our store. There’s two ways to do this, both very simple:

// Containers/App.js
import React, {Component} from 'react';
import {connect } from 'react-redux';
import Counter from '../components/Counter';

@connect(state => ({
  count: state.counter
}))
export default class App extends Component {
  render() {
    return (
      <Counter count={this.props.count} />
    );
  }
}

In this example we wrap our top level component with a connect decorator. We pass a callback that receives the state, this state is an object that contains the previously created Counter reducer. We return an object in which we map the reducer’s state to a count value. Our App component will receive this count via its props, which we can then pass to our “dumb” Counter component.

Now that our Counter component is using the data in our store we can make some changes to it:

// Components/Counter.js
import React, {Component} from 'react';

export default class Counter extends Component {
  static defaultProps = {
    increase: 1
  }

  render() {
    const {count, increase} = this.props;

    return (
      <div>
        <p>The button has been clicked {count} times.</p>
        <button>+{increase}</button>
      </div>
    );
  }
}

We got rid of the internally managed state and are now using the count passed via the props.

Adding interactivity

Our last step is to add interactivity to our counter. Like I mentioned before Flux (and Redux): use actions to modify the state. An action is a simple javascript object. So for our counter it could be as simple as this:

{
  type: 'increment',
  amount: 1,
};

Now obviously this is kind of static and we can’t really do much with it yet, we need to have a way to trigger these actions. This is where action creators come in. These are simply functions that return an action. Like this:

// Actions/Counter.js
import Api from '../Api';

export function increment(amount = 1) {
  return {
    type: 'increment',
    amount,
  };
};

export function sync(count) {
  return dispatch => {
      Api.sync(count).then(() => {
        dispatch({
          type: 'sync',
        });
      });
  };
}

Then we need to make our application aware of the action. We can do that like this:

// Containers/App.js
import React, {Component} from 'react';
import {connect } from 'react-redux';
import Counter from '../components/Counter';
import {increment} from '../Actions/Counter';

@connect(state => ({
  count: state.counter
}))
export default class App extends Component {
  render() {
    const {dispatch} = this.props;

    return (
      <Counter
        count={this.props.count}
        increment={amount =>
          dispatch(increment(amount))
        }
      />
    );
  }
}

For small apps this works fine, but imagine having a lot of actions. Managing that would become a nightmare. Luckily Redux has a helper that solves this for us, called bindActionCreators.

import React, {Component} from 'react';
import {bindActionCreators} from 'redux';
import {connect } from 'react-redux';
import Counter from '../components/Counter';
import * as actions from '../actions/counter';

@connect(state => ({
  count: state.counter
}))
export default class App extends Component {
  render() {
    return (
      <Counter count={this.props.count} actions={bindActionCreators(actions, this.props.dispatch)}/>
    );
  }
}

This will bind all our action creators to the dispatcher and expose them via an object. Our Counter component will now receive an actions object via it’s props containing the increment action creator. Now all that is left to do is trigger the action when clicking the button:

// Components/Counter.js
import React, {Component} from 'react';

export default class Counter extends Component {
  static defaultProps = {
    increase: 1
  }

  render() {
    const {count, increase, actions: {increment}} = this.props;
    return (
      <div>
        <p>The button has been clicked {count} times.</p>
        <button onClick={() => increment(increase)}>+{increase}</button>
      </div>
    );
  }
}

And then finally we need to add this action to our reducer:

// Reducers/Counter.js
export default function counter(state = 0, action) {
  switch(action.type) {
    case 'increment':
      return state + action.amount;
    default:
      return state;
  }
}

And that’s it, we now have an application where the state is kept in a single source that can only be mutated by triggering actions.

Benefits of Redux

Because we use Redux we can now use some very nice tools that improve our development.

Redux devtools

One of those tools is redux-devtools which gives you a visual overview of all the actions happening in your application. Adding it to our application requires some minor changes:

npm install --save-dev redux-devtools

First of all we need to adapt our store:

// Stores/index.js
import {compose, createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import {devTools} from 'redux-devtools';
import reducers from '../Reducers';

export default function store(initialState) {
  let middlewares = [
    applyMiddleware(thunk),
    devTools(),
  ];

  const store = compose(...middlewares)(createStore);

  return store(reducers, initialState);
}

You can see how we added the devTools middleware to our stack.

The devtools come with their own React component which we can include, we will create a new top level component for this.

// Containers/ReduxDevtools
import React from 'react';
import {DevTools, DebugPanel, LogMonitor} from 'redux-devtools/lib/react';

export default function devtools(store) {
  const stateSelector = state => state;

  return (
    <DebugPanel top right bottom key="devtools">
      <DevTools select={stateSelector} store={store} monitor={LogMonitor} />
    </DebugPanel>
  );
}

And finally we need to render the devtools in our browser:

// index.js
import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import createStore from './Stores';
import App from './Containers/App';
import devtools from './Containers/ReduxDevtools';

const store = createStore();

render(
  <div>
    <Provider store={store}>
      <App />
    </Provider>
    {devtools(store)}
  </div>,
  document.getElementById('app'));

The result looks like this:

Devtools displaying all our actions

We can now track all the actions with their payloads and how they affect the state in this monitor. Do note that you should disable this in your production environment, since it impacts the performance. Our favorite way of doing this is by using environment variables with webpack.


All of the above examples are available on Github:

Comments