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!