Setting up Flux in a React app


This post will go over how to set up Flux in a React app.

View

Starting with the component:

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

export default class Counter extends Component {
  state = {
    count: 0,
  };

  increment = () => {
    this.setState({
      count: this.state.count + 1,
    });
  };

  decrement = () => {
    this.setState({
      count: this.state.count - 1,
    });
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.increment}>+</button>
        <button onClick={this.decrement}>-</button>
      </div>
    );
  }
}

The data flow can be described by the diagram:

 ---------------      ----------------      ------------------      --------------
| button (view) | -> | click (action) | -> | setState (store) | -> | count (view) |
 ---------------      ----------------      ------------------      --------------

When view components share state, then it’s a good time to add a store.

Store

Create a store that extends EventEmitter:

// countStore.js
import { EventEmitter } from 'events';

let count = 0;

class CountStore extends EventEmitter {
  getCount() {
    return count;
  }

  increment = () => {
    count++;
    this.emit('change');
  };

  decrement = () => {
    count--;
    this.emit('change');
  };
}

export default new CountStore();

EventEmitter is used to set up a publish-subscribe (pub/sub) messaging pattern.

When increment or decrement is called, the 'change' event is emitted.

Wire up the component state with the store state:

// Counter.js
// ...
import countStore from './countStore';

export default class Counter extends Component {
  state = {
    count: countStore.getCount(),
  };

  componentDidMount() {
    countStore.on('change', this.updateCount);
  }

  componentWillUnmount() {
    // remove or else there will be memory leaks
    countStore.off('change', this.updateCount);
  }

  updateCount = () => {
    this.setState({
      count: countStore.getCount(),
    });
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={countStore.increment}>+</button>
        <button onClick={countStore.decrement}>-</button>
      </div>
    );
  }
}

componentDidMount adds the change listener and componentWillUnmount removes the change listener.

To minimize unexpected side effects when the code is changed, refactor the listeners into helper methods:

// countStore.js
// ...

class CountStore extends EventEmitter {
  /** @param {Function} callback */
  addChangeListener(callback) {
    this.on('change', callback);
  }

  /** @param {Function} callback */
  removeChangeListener(callback) {
    this.off('change', callback);
  }
  // ...
}

Update the listeners in the component:

// Counter.jsx
// ...

export default class Counter extends Component {
  componentDidMount() {
    countStore.addChangeListener(this.updateCount);
  }

  componentWillUnmount() {
    countStore.removeChangeListener(this.updateCount);
  }
  // ...
}

We can add a dispatcher next. However, the best time to add one is when the number of stores increase. This is so a single dispatcher dispatches all actions to the stores.

Dispatcher

Instantiate a dispatcher from flux:

// dispatcher.js
import { Dispatcher } from 'flux';

export default new Dispatcher();

The dispatcher broadcasts payloads to its registered callbacks.

Thus, register the dispatcher in the store:

// countStore.js
// ...
import dispatcher from './dispatcher';

class CountStore extends EventEmitter {
  constructor() {
    super();
    this.dispatchToken = dispatcher.register((action) => {
      switch (action.type) {
        case 'increment':
          this.increment();
          break;
        case 'decrement':
          this.decrement();
          break;
        default:
          break;
      }
    });
  }
  // ...
}

The dispatch method receives an argument action. In our example, it’s simply an object.

The action object can contain any type of property, but type is used here as per convention.

Additional data can be passed via the property payload, which is also named as per convention.

Replace the store methods with dispatch:

// Counter.js
// ...

export default class Counter extends Component {
  // ...
  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={() => dispatcher.dispatch({ type: 'increment' })}>
          +
        </button>
        <button onClick={() => dispatcher.dispatch({ type: 'decrement' })}>
          -
        </button>
      </div>
    );
  }
}

It seems that the dispatch calls are getting repetitive.

For the next step, we’ll add action creators so we don’t have to call the dispatcher directly in our component.

Actions

Create the actions:

// actions.js
import dispatcher from './dispatcher';

export const increment = () => {
  dispatcher.dispatch({
    type: 'increment',
  });
};

export const decrement = () => {
  dispatcher.dispatch({
    type: 'decrement',
  });
};

export default {
  increment,
  decrement,
};

Replace them with the dispatcher:

// Counter.js
// ...
import actions from './actions';

export default class Counter extends Component {
  // ...
  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={actions.increment}>+</button>
        <button onClick={actions.decrement}>-</button>
      </div>
    );
  }
}

Since we’re using the same action types in multiple files, we can refactor them into constants to keep the code DRY.

Constants

Action types are simply strings:

// constants.js
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';

The convention is to use the uppercase string of the variable name.

Now update actions with the constants:

// actions.js
// ...
import { INCREMENT, DECREMENT } from './constants';

export const increment = () => {
  dispatcher.dispatch({
    type: INCREMENT,
  });
};

export const decrement = () => {
  dispatcher.dispatch({
    type: DECREMENT,
  });
};

And update the store as well:

// countStore.js
// ...
import { INCREMENT, DECREMENT } from './constants';

class CountStore extends EventEmitter {
  constructor() {
    super();
    // the token can be used to wait or synchronize with the other stores
    this.dispatchToken = dispatcher.register((action) => {
      switch (action.type) {
        case INCREMENT:
          this.increment();
          break;
        case DECREMENT:
          this.decrement();
          break;
        default:
          break;
      }
    });
  }
  // ...
}

At last, we have successfully wired up our React app with Flux!



Please support this site and join our Discord!