Fluxify
Yet another thing that ends with "ify": The simplest Flux implementation.
It is simple because:
- It has no dependencies.
- It is compatible with facebook's flux dispatcher.
- It can be required as a page script, AMD or CommonJS module.
- It handles asynchronous actions using promises.
- It has eventful stores out of the box.
- It has pseudo-immutable stores, so they only can be modified by actions.
- It is lightweight: <5KB minified (much less if gzipped).
Here it an example:
//Let's suppose browserify
var flux = require('fluxify');
// Create a store
var helloStore = flux.createStore({
id: 'helloStore',
initialState: {
name: 'Alice'
},
actionCallbacks: {
changeName: function( updater, name ){
// Stores updates are only made inside store's action callbacks
updater.set( {name: name} );
}
}
});
// It is not possible to update the store directly
helloStore.name = 'Charlie';
console.log( helloStore.name ); // logs "Alice"
// Listen to name changes
helloStore.on( 'change:name', function( name, previousName ){
console.log( 'Bye ' + previousName + ', hello ' + helloStore.name );
});
// Dispatch actions
flux.doAction( 'changeName', 'Bob' ); // logs "Bye Alice, hello Bob";
Index
Installation
Fluxify is available as a npm package.
npm install fluxify
Also as a bower package
bower install fluxify
It is possible to download the full version (~13KB) or minified (~5KB)
Use
Fluxify is packed as a UMD module, so it is possible to require it:
- As a CommonJS module (Node/Browserify)
var flux = require('fluxify');
- As a AMD module( require.js )
require(['fluxify'], function( flux ){
...
});
- With a script tag in any web page
<script src="path/to/fluxify.js"></script>
In this last case fluxify will be globally available.
Docs
Fluxify brings all the necessary to start with flux architecture immediately: dispatcher, stores and actions.
Below it is all the info needed to know how Fluxify works.
Dispatcher
Using Flux architecture your application has a main state and the UI reflects that state. When the user interact with the app ( or there are updates from the server ) an action is launched. The disptacher broadcasts the action data to the all registered callbacks and they update the application state depending on that data.
In Fluxify the dispatcher is a singleton, as recommended by Facebook, and you can access to it through the fluxify object.
var dispatcher = require('fluxify').dispatcher;
It is possible to use it like Facebook's dispatcher implementation, registering action callbacks and dispatching payloads.
var callbackId = dispatcher.register( function( payload ){
if( payload.actionType = 'log' )
console.log( 'I was logged by an action!' );
});
dispatcher.dispatch( {actionType: 'log'} ); // log 'I was logged by an action!'
An asynchronous dispatcher
The main difference of Fluxify's dispatcher is that it can handle asynchronous actions, those actions that return a promise. dispatch
and waitFor
methods also return a promise that fulfill when the dispatch or the callbacks are finished.
var cb1 = dispatcher.register( function( payload ){
return dispatcher.waitFor( [cb2] )
.then( function(){
console.log( 'I was waiting by cb2' );
})
;
});
var cb2 = dispatcher.register( function( payload ){
return new dispatcher._Promise( function( resolve, reject ){
setTimeout( function(){
console.log( 'cb1 has been waiting by me' );
resolve();
});
});
});
dispatcher.dispatch( {actionType: 'whatever'} )
.then( function(){
console.log( 'Everything dispatched properly' );
})
;
// logs...
// cb1 hasbeen waiting by me
// I was waiting by cb2
// Everything dispatched properly
Since a dispatch can take a while, it is possible to call a new dispatch before the previous dispatch call had finished. Instead of throwing an error, the new call will be enqueued and executed just after the current dispatch finishes.
Dispatcher promises
Fluxify needs ES6 Promises to work. If your environment supports promises ( current Chrome, Firefox, Opera and Safari versions support it ) or you use any polyfill everything will be ok.
But if the enviroment doesn't have the global Promise
object defined, it is needed to provide Promise
explicity, using Fluxify's promisify
method.
// Using Jake Archibald's es6 promise implementation
var flux = require('fluxify'),
Promise = require('es6-promise').Promise
;
// Let's get async!
flux.promisify( Promise );
There are a bunch promises implementations you can use. Popular choices are es6-promise, when.js or bluebird.js.
Setting your own ids for callbacks
Using fluxify it is possible to define your own IDs for the callbacks.
In the file clbk1.js
// Pass a string as the first parameter, it will be used as id
dispatcher.register('clbk1', function(){
return dispatcher.waitFor( 'clbk2' )
.then( function(){
console.log('I was waiting by clbk2');
})
;
});
In the file clbk2.js
dispatcher.register('clbk2', function(){
console.log('Here I am!');
});
This feature allows to decouple the callbacks from the ID given by the dispatcher. In the example above, we don't need to save the id generated in the file clbk2.js and pass it to the callback in the file clbk1.js, because we already know that the id will be clbk2
, and we can use it direcly in the waitFor
method.
Great power comes with great responsibility, if you give your own ids to the callbacks there is risk of name collision, so it is up to you the way to track your callback names (using a constants file?).
Dispatch many arguments
Another difference between Facebook and Fluxify dispatcher is the number of arguments to dispatch. Facebook implementation only dispatches one argument, the payload object, while Fluxify dispatches all the arguments that are passed to the dispatch
function.
That is a key feature to make actions much easier to use, as it is explained in the action section.
var dispatcher = require('fluxify').dispatcher;
// Register a callback that log the arguments
dispatcher.register( function(){
console.log( arguments );
});
// It is possible to use dispatches as usual
dispatcher.dispatch( {actionType: 'logAll'} ); // logs [{actionType: 'logAll'}]
// Or you can dispatch multiple data
dispatcher.dispatch( 'logAll', 1, 2, 3 ); // logs ['logAll', 1, 2, 3]
Fluxify's stores make use of the multiple arguments dispatching to make easier to register their callbacks.
Actions
Actions are originated by the user interaction with the application or by some server event. The dispatcher broadcasts the action and its data to the registered callbacks and they know how to react to those actions updating the stores.
In Fluxify there are no action objects, but the concept of Action is important in Flux architecture, so the fluxify object expose a doAction
method to explicity remark that we are executing an action.
var flux = require('fluxify');
flux.dispatcher.register( function( payload ){
if( payload.actionType == 'sayHello')
console.log( 'Hello ' + payload.name + '!' );
});
flux.doAction( {actionType: 'sayHello', name: 'John'} ); // logs "Hello John!"
Action creators
Facebook recommends to create methods with specific names for executing actions. That makes the code in the view be more semantic, and easier to write than dispatching a payload:
// Somewhere in your app...
var actions = {
hello: function(){
flux.doAction( {actionType: 'sayHello'} );
}
}
// Somewhere in your view code...
actions.hello('Amy');
// Much more readable than
// flux.doAction( {actionType: 'sayHello', name: 'John'} );
In different Flux implementations, the objects that store the actions are called action creators. Thinking in the fluxify way they are not needed either, since the dispatches accept multiple the arguments, it is much easier to use the doAction
method directly with the data that the callback needs as arguments.
Let's fluxify:
var flux = require('fluxify');
flux.dispatcher.register( function( actionName, name ){
if( actionName == 'sayHello')
console.log( 'Hello ' + name + '!' );
});
flux.doAction( 'sayHello', 'John' ); // logs "Hello John!"
The example above is shorter and easier to write and understand than the original one, and it is as semantic as the one that uses action creators.
In fluxify, the use of multiple-argument dispatches is preferred to the use of verbose payloads.
Stores
The stores are the source of truth of your application. They keep the state of parts of your application, and your UI should be the graphical representation of those states. If something change in a store, the UI should react updating the related parts to reflect that state change.
Fluxify's stores emit events when they are updated, so the app's views can listen to this events to automatically apply those changes to the UI.
var flux = require('fluxify'),
React = require('react')
;
// Our store
var nameStore = flux.createStore({
id: 'nameStore',
initialState: {
name: 'Alice'
},
actionCallbacks: {
changeName: function( updater, name ){
// We can update nameStore with the set method
// of the first argument
updater.set( {name: name} );
}
}
});
// Let's create a react component
var Hello = React.createClass({
getInitialState: function(){
return {name: this.props.name };
},
render: function() {
return <div>Hello {this.state.name}</div>;
},
componentDidMount: function(){
var me = this;
nameStore.on( 'change:name', function( value ){
me.setState( {name: value} );
});
//Let's update the name in 2 seconds
setTimeout( function(){
flux.doAction( 'changeName', 'Bob' );
}, 2000);
}
});
React.render(<Hello name={nameStore.name} />, document.body);
Store initialize options
The code above is a complete example of stores in Fluxify. Stores are created using Fluxify's factory method createStore
with an options object. Available options are:
- id {string}: When an id is set in the options object, store's callback is automatically registered in the dispatcher with that id.
var flux = require('fluxify');
var myStore = flux.createStore( {id: 'myStore'} );
// Is equivalent to...
var myStore = flux.createStore();
flux.dispatcher.registerStore( 'myStore', myStore );
flux.stores.myStore = myStore;
As you can see, a reference to the store is saved in the stores attribute of the flux object too.
- initalState {object}: The initial properties of the store. A Fluxify's store can be see as a common JS object, and you can read their properties as usual:
var myStore = flux.createStore({
initialStore: {
a: 1,
b: 2,
c: 3
}
});
console.log( myStore.a ); // logs 1
console.log( myStore.b ); // logs 2
console.log( myStore.c ); // logs 3
- actionCallbacks {object}: The functions in this object will be registered in the dispatcher, and called when the action named as the key is triggered.
There are two ways of call these callbacks:
- Passing the name as the first argument of Fluxify's
doAction
method. - Passing the name as the
actionType
attribute of the payload.
- Passing the name as the first argument of Fluxify's
var myStore = flux.createStore({
id: 'myStore',
actionCallbacks: {
myAction: function( updater ){
console.log( 'Hi!');
}
}
});
// These two action calls will trigger the store's callback
flux.doAction( 'myAction' );
flux.doAction( {actionType: 'myAction'} );
Store updater and events
Callbacks will receive always a updater
object as the first argument. Using that object is the only way of updating a store, and it is only available in action callbacks forcing the developer to update the stores only there, as the Flux architecture recommends.
To update the store, use the updater.set
method, it will change store's property values and emit events that can be listener by the rest of the application.
The rest of the arguments received are the ones given to doAction
.
In every update, two events are emitted by the store:
-
change:{updatedProperty}
with the new and old values as parameters for the listeners. -
change
with an array of objects{prop, value, previousValue}
for every property updated.
var myStore = flux.createStore({
id: 'myStore',
initialState: { a: 1, b: 2 },
actionCallbacks: {
myAction: function( updater, a, b, c ){
updater.set({
a: a,
b: b,
c: c
});
}
}
});
console.log( myStore.getState() ); // 1
// Set up some event listeners
myStore.on( 'change:a', function( value, previousValue ){
console.log( 'a value was ' + previousValue + ' and now it is ' + value ); // 2
});
myStore.on( 'change', function( updates ){
// updates will have 3 elements,
// one for each property updated
console.log( updates[0] ); // 3
});
// This will emit the events
// 'change:a', 'change:b', 'change:c' and 'change'
flux.doAction( 'myAction', 3, 2, 1 )
.then( function(){
console.log( myStore.getState() ); // 4
})
;
// The console will show;
// logs {a:1, b:2}
// a value was 1 and now it is 3
// { prop: 'a', value: 3, previousValue: 1}
// {a:3, b:2, c:1}
Pseudo-immutable stores
It is not possible to change the value of a store's property directly, because they are defined as non-writable. But be aware that the store itself is mutable, you can add new properties directly but that is not recomended, since that property changes won't emit any events.
var myStore = flux.createStore({
intialState: {a:1}
});
// Properties are immutable
myStore.a = 5;
console.log( myStore.a ); // logs 1
// But you can add new ones
// This won't emit any event
myStore.b = 1;
console.log( myStore.b ); // logs 1
myStore.b = 2;
console.log( myStore.b ); // logs 2
Also keep in mind that if the property value is an object (or an array, they are objects too) the reference to that object is immutable, but the object itself is not.
var myStore = flux.createStore({
initialState: {
ob: {a: 1}
}
});
// It is not possible to update the reference
myStore.ob = 'Hey';
console.log( myStore.ob ); // {a: 1}
// Be careful, you can update the object itself
// and that won't emit any event
myStore.ob.b = 2;
console.log( myStore.ob ); // {a:1, b:2}
Stores in fluxify are pseudo-immutable to encourage their updates to be done inside the action callbacks, the way recommended by Flux architecture, but it is still possible to do the above bad practices.
More about stores
- In store's action callbacks the
this
object references to the store.
var myStore = flux.createStore({
initialState: { a: 1 },
actionCallbacks: {
myAction: function(){
console.log( this.a ); // logs 1
}
}
});
- Registered stores has a
waitFor
method for convenience.
var fooStore = flux.createStore({
id: 'foo',
actionCallbacks: {
myAction: function(){
return this.waitFor( 'bar' )
.then( function(){
console.log( 'foo' );
})
;
}
}
});
var barStore = flux.createStore({
id: 'bar',
actionCallbacks: {
myAction: function() {
console.log( 'bar' );
}
}
}
});
flux.doAction('myAction');
// logs:
// 'bar'
// 'foo'
- Action callbacks can be added after initialization using
addActionCallbacks
var myStore = flux.createStore();
myStore.addActionCallbacks({
myAction: function(){
console.log( 'Hey' );
}
});
flux.doAction( 'myAction' ); // logs 'Hey'
- A writable copy of the object is available using
getState
method. It is the preferred way of passing a store to a react component.
var nameStore = flux.createStore({
initialState: {
name: 'Alice'
}
});
React.createElement( 'div', {store: nameStore.getState()} );
Collaborate
Fluxify is a young library, so PRs, issues and feature requests or anything that helps it to improve is welcome.