Reflux
A simple library for uni-directional dataflow architecture inspired by ReactJS Flux.
You can read an overview of Flux here, however the gist of it is to introduce a more functional programming style architecture by eschewing MVC like pattern and adopting a single data flow pattern.
╔═════════╗ ╔════════╗ ╔═════════════════╗
║ Actions ║──────>║ Stores ║──────>║ View Components ║
╚═════════╝ ╚════════╝ ╚═════════════════╝
^ │
└──────────────────────────────────────┘
The pattern is composed of actions and data stores, where actions initiate new data to pass through data stores before coming back to the view components again. If a view component has an event that needs to make a change in the application's data stores, they need to do so by signalling to the stores through the actions available.
The goal of the project is to get this architecture easily up and running in your web application, both client-side or server-side. There are some differences between how this project works and how Facebook's proposed Flux architecture works:
- Instead of a singleton dispatcher, every action handles the dispatching by themselves
- Javascript is a dynamic language so no more static type checking with strings! Just let the stores listen to actions and don't worry!
You can read more in this blog post about React Flux vs Reflux.
Installation
You can currently install the package as a npm package or bower.
NPM
The following command installs reflux as an npm package:
npm install reflux
Bower
The following command installs reflux as a bower component that can be used in the browser:
bower install reflux
Usage
For a full example check the test/index.js
file.
Creating actions
Create an action by calling Reflux.createAction
.
var statusUpdate = Reflux.createAction();
An action is a functor that can be invoked like any function.
statusUpdate();
It is as simple as that. There is also a convenience function for creating multiple actions.
var Actions = Reflux.createActions([
"statusUpdate",
"statusEdited",
"statusAdded"
]);
Actions.statusUpdate();
Action hooks
There are a couple of hooks avaiable for each action.
-
preEmit
- Is called before the action emits an event. It receives the arguments from the action invocation.
-
shouldEmit
- Is called after preEmit
and before the action emits an event. By default it returns true
which will let the action emit the event. You may override this if you need to check the arguments that the action receives and see if it needs to emit the event.
Example usage:
Actions.statusUpdate.preEmit = function() { console.log(arguments); };
Actions.statusUpdate.shouldEmit = function(value) {
return value > 0;
};
Actions.statusUpdate(0);
Actions.statusUpdate(1);
Creating data stores
Create a data store much like ReactJS's own React.createClass
by passing a definition object to Reflux.createStore
. You may set up all action listeners in the init
function and register them by calling the store's own listenTo
function.
var statusStore = Reflux.createStore({
init: function() {
this.listenTo(statusUpdate, this.output);
},
output: function(flag) {
var status = flag ? 'ONLINE' : 'OFFLINE';
this.trigger(status);
}
});
In the above example, whenever the action is called, the store's output
callback will be called with whatever parameters was sent in the action. E.g. if the action is called as statusUpdate(true)
then the flag argument in output
function is true
.
Listening to changes in data store
In your component, register to listen to changes in your data store like this:
function ConsoleComponent() {
statusStore.listen(function(status) {
console.log('status: ', status);
});
};
var consoleComponent = new ConsoleComponent();
Invoke actions as if they were functions:
statusUpdate(true);
statusUpdate(false);
With the setup above this will output the following in the console:
status: ONLINE
status: OFFLINE
React component example
Register your component to listen for changes in your data stores, preferably in the componentDidMount
lifecycle method and unregister in the componentWillUnmount
, like this:
var Status = React.createClass({
initialize: function() { },
onStatusChange: function(status) {
this.setState({
currentStatus: status
});
},
componentDidMount: function() {
this.unsubscribe = statusStore.listen(this.onStatusChange);
},
componentWillUnmount: function() {
this.unsubscribe();
},
render: function() {
}
});
Convenience mixin for React
You always need to unsubscribe components from observed actions and stores upon
unmounting. To simplify this process you can use mixins in React. There is a convenience mixin available at Reflux.ListenerMixin
.
var Status = React.createClass({
mixins: [Reflux.ListenerMixin],
onStatusChange: function(status) {
this.setState({
currentStatus: status
});
},
componentDidMount: function() {
this.listenTo(statusStore, this.onStatusChange);
},
render: function() {
}
});
The mixin provides the listenTo
method for the React component, that works much like the one found in the Reflux's stores, and handles the listeners during mount and unmount for you.
Listening to changes in other data stores (aggregate data stores)
A store may listen to another store's change, making it possible to safely chain stores for aggregated data without affecting other parts of the application. A store may listen to other stores using the same listenTo
function as with actions:
var statusHistoryStore = Reflux.createStore({
init: function() {
this.listenTo(statusStore, this.output);
this.history = [];
},
output: function(statusString) {
this.history.push({
date: new Date(),
status: statusString
});
this.trigger(this.history);
}
});
Advanced usage
Switching EventEmitter
Don't like to use the EventEmitter provided? You can switch to another one, such as NodeJS's own like this:
Reflux.setEventEmitter(require('events').EventEmitter);
Switching nextTick
Whenever action functors are called, they return immediately through the use of setTimeout
(nextTick
function) internally.
You may switch out for your favorite setTimeout
, nextTick
, setImmediate
, et al implementation:
Reflux.nextTick(process.nextTick);
For better alternative to setTimeout
, you may opt to use the setImmediate
polyfill.
Joining parallel listeners with composed listenables
Reflux makes it easy to listen to actions and stores that emit events in parallel. You can use this feature to compose and share listenable objects (composed listenables) among several stores.
var theTide = Reflux.all(waveAction, timeStore);
var clockStore = Reflux.createStore({
init: function() {
this.listenTo(theTide, this.theTideCallback);
},
theTideCallback: function(waveActionArgs, timeStoreArgs) {
}
});
if (process.env.DEVELOPMENT) {
theTide.listenTo(console.log.bind(console), window);
}
Reflux.all
always passes the last arguments that a listenable emitted to your callback, discarding subsequent emits. Arguments are passed in order. This means that the first argument which the callback receives, is the set of arguments which was emitted by the first listenable that was passed to Reflux.all
and so on for the other arguments.
Comparison with Flux's waitFor()
The Reflux.all
functionality is similar to Flux's waitFor()
, but differs in a few aspects:
- Composed listenables may be reused by other stores
- Composed listenables always emit asynchronously
- Actions and stores may emit multiple times before the composed listenable (
theTide
in the example above) emits - Action and store callbacks are not executed in a single synchronous iteration
Sending default data with the listenTo function
The listenTo
function provided by the Store
and the ListenerMixin
has a third parameter that accepts a callback. This callback will be invoked when the listener is registered with whatever the getInitialData
is returning.
var exampleStore = Reflux.createStore({
init: function() {},
getInitialData: function() {
return "the initial data";
}
});
this.listenTo(exampleStore, onChangeCallback, initialCallback)
Colophon
List of contributors is available on Github.
This project is licensed under BSD 3-Clause License. Copyright (c) 2014, Mikael Brassman.
For more information about the license for this particular project read the LICENSE.md file.
This project uses eventemitter3, is currently MIT licensed and has it's license information here.