lux.js is an implementation of a Flux architecture using ReactJS, postal.js and machina.js. In a nutshell, the React components, dispatcher and stores are highly de-coupled. Here's a gist of the opinions at play:
- Components use an ActionCreator API as a proxy to stores.
- ActionCreator APIs generate message payloads that are routed to the Dispatcher, which coordinates when and how the stores are told to handle the actions.
- Store operations are synchronous.
- Store state is not mutated directly. In fact, it's only possible to mutate store state inside a store's action handler. Communication with a store is done via the Dispatcher (using an action creator API), or an equivalent message-capable API that follows the lux message contracts for these operations.
- Other modules can take a dependency on a store for read-only operations.
- Once all stores involved in processing an action have completed their tasks, the Dispatcher signals (via msg) that it's OK for the stores to send change notifications.
- ControllerViews (see below) that are wired up to listen to specific stores will receive their updates, etc.
- All communication happens via message passing.
- Each message payload should be fully serializable(!)
A ControllerView is a component that contains state that will be updated from a store - they will typically appear at the top (or near) of logical sections of your component tree. In lux, a ControllerView gets two primary mixins: lux.reactMixin.actionCreator
and lux.reactMixin.store
(see the section on mixins below for more information on the mixins' API). The actionCreator
mixin gives the ControllerView an ActionCreator
API for the specified action(s) or action group(s). The store
mixin wires the component into the bus to listen for updates from the specified store(s).
You can get an instance of a ControllerView by calling lux.controllerView()
(You must call lux.initReact( React )
once in your app before using this method). For example, the ControllerView below is being given all the actions associated with the "board" action group, and is also listening to the board store for data:
var lux = require( "lux.js" );
// Only needed one time in the app
// (& only if you're using lux.controllerView or lux.component)
lux.initReact( React );
var LaneSelector = lux.controllerView({
// In this case "board" is an action group that contains
// at least one action called "loadBoard". Any actions
// under the "board" action group get added as top level
// methods to this react element.
getActionGroup: ["board"],
stores: {
// `listenTo` can also be an array of store namespaces
// like [ "board", "card", "user" ], etc.
// We're only listening to one store here, so we're
// using the string namespace shortcut to specify it
listenTo: "board",
// The `boardStore` used below is a module dependency for this
// element. It contains a helper method (read accessor) which we're
// invoking to get the specific board data we want. If you're listening
// to multiple stores, the onChange handler will not fire until
// all the stores have updated.
onChange: function() {
var newState = boardStore.getBoard(this.props.boardId);
this.setState(newState);
}
},
getInitialState: function() {
return {
lanes: []
};
},
componentWillMount: function() {
this.loadBoard(this.props.boardId);
},
render: function() {
//etc etc, you get the idea
}
});
Lux provides a couple of helper methods to get pre-configured React components, but you can also stick to plain React components, and use the appropriate lux mixins. Since lux.controllerView
is just a convenience method that calls React.createClass
with the two mixins, you could also use:
var LaneSelector = React.createClass({
mixins: [ lux.reactMixin.store, lux.reactMixin.actionCreator ],
...
});
The nice thing about the above approach is that if you just need to listen to a store for state, then you could include only the mixin you need:
var LaneSelector = React.createClass({
mixins: [ lux.reactMixin.store ],
...
});
A lux ControllerView will wait on all the stores involved in an action to signal state changes before it calls onChange
. This means your component will only call render once even if it's listening to multiple stores that change state during an action dispatch.
For components that need an ActionCreator API (i.e. - they need to dispatch actions), but don't need to listen to a store, you can call lux.component()
(You must call lux.initReact( React )
once in your app before using this method). For example, this component is being given an ActionCreator API for the "board" store (which adds the toggleLaneSelection
method to the component), but is NOT listening to the board store for state, since it's not a ControllerView:
var lux = require( "lux.js" );
// Only needed one time in the app
// (& only if you're using lux.controllerView or lux.component)
lux.initReact( React );
var Lane = lux.component({
getActionGroup: ["board"],
getInitialState: function() {
return {
isCollapsed: false
};
},
getDefaultProps: function() {
return {
items: [],
siblingSize: 1,
depth: 0,
isParentActive: false
};
},
toggleActive: function(e) {
e.stopPropagation();
this.toggleLaneSelection(this.props.boardId, this.props.key);
},
render: function() {
// etc etc, you get the idea....
}
});
Just like lux.controllerView
, lux.component
is a convenience method that calls React.createClass
with one mixin. You could also use:
var Lane = React.createClass({
mixins: [ lux.reactMixin.actionCreator ],
...
});
The Dispatcher listens for actions that are dispatched by Components using ActionCreator APIs. Underneath, it's a BehavioralFSM
. You don't have to do anything special, lux creates a single dispatcher instance for you.
A store should contain your data as well as any business logic related to that data. A lux Store can be created by using the Store constructor (i.e. - var store = new lux.Store({ /* options */ })
. A store should always have a namespace
(just a logical name for it) and it will have any number of action handlers
. What you name an action handler on a store is important, as the same name will be used on the corresponding ActionCreator API. If a store action handler returns false explicitly, it tells the action coordinator that the state of the store didn't change. However, returning undefined or any other value will result in the action coordinator assuming a change to the store state occurred during the handler's operations. Stores contain the following methods:
getState
- synchronously returns the state of a store.setState
- works much like a component'ssetState
method (via shallow extending/assigning) - synchronously updates state of a store. It's only possible to invoke this method from inside a store action handler.flush
- causes a change notification to be sent out (via message) if the store's state has changed.dispose
- removes all message bus subscriptions for a store, effectively disabling it from being used any further.
For example, the store below has a namespace of "board", and it contains two action handlers - toggleLaneSelection
and loadBoard
, as well as two "read accessor" helper methods call getCurrentBoard
and getBoard
:
var boardStore = new lux.Store( {
namespace: "board",
handlers: {
toggleLaneSelection: function( boardId, laneId ) {
var newState = this.getState();
var target = newState[ boardId ] && newState[ boardId ].lookup[ laneId ];
if ( target ) {
target.isActive = !target.isActive;
toggleAncestors( newState[ boardId ].lookup, target );
toggleDescendants( target );
this.setState(newState);
}
},
boardLoaded: function( boardId, board ) {
var newState = this.getState();
newState[ boardId ] = parser.transform( board );
newState._currentBoardId = boardId;
this.setState(newState);
}
},
getCurrentBoard: function() {
var state = this.getState();
return state[state._currentBoardId];
},
getBoard: function(id) {
return this.getState()[id];
}
} );
It's possible to pass multiple "mixins" to the Store
constructor, should the need arise. It supports deep-merging of state as well as handlers. (Yes, handlers!) If multiple mixins target the same action handler, both handler methods will be queued to execute when the store handles the action (they are queued in the same order they are mixed-in). If you are passing mixins to an extended constructor, your handlers will be queued after any handlers of the same name that are inherited from the extended constructor, etc. When merging state, lodash currently follows a "first-write-wins" approach with arrays.
var boardStore = new lux.Store(
{ namespace: "board" },
boardLoadedMixin,
laneSelectionMixin,
apiLoadedMixin
);
Components don't talk to stores directly. Instead, they use an ActionCreator API as a proxy to store operations. ActionCreator APIs are built automatically as stores (and any other instance that uses the lux.mixin.actionListener
mixin) are created. If you use the actionCreator
mixin (or use a ControllerView or lux Component), action creator APIs will appear as top level methods on your component. The auto-generated ActionCreator APIs use the same method names as the corresponding action type names. ActionCreator methods simply publish the correct message payload for the action. Remember that any arguments you pass to an ActionCreator method should be fully serializable. If you really need to, you can create your own ActionCreator API method - either at the component level, or globally. Implementing a method on your component that matches the name of an action allows for a component-specific override, or you can use lux.customActionCreator
to implement your own global overrides of how an action is dispatched. You just have to honor the message contracts (see the source for information on that now....more documentation later).
You can group actions together under a "label" (known as an action group) by using the lux.addToActionGroup
method:
// if the "board" action group doesn't exist, it will be created. If it does exist
// the actions in the second argument will be added to what's already there
lux.addToActionGroup( "board", ["toggleLaneSelection", "toggleRollUpLane", "loadBoard" ]);
By default, lux will generate an action creator method for your actions (which simply publishes a structured message payload defining the action), but you might want to create a custom version of an action creator method:
// a contrived example of creating a custom action creator method that console.logs
// the arguments before publishing the action message like the auto-generated method would do
lux.customActionCreator({
myActionName: function(something, anotherThing) {
console.log("I want to console log this:", something, anotherThing);
lux.publishAction("myActionName", something, anotherThing);
}
});
The actionCreator
mixin (which is provided automatically when you call lux.controllerView
and lux.component
) will look for a getActionGroup
array or a getActions
array on your component options. The getActionGroup
array should contain the string action group name(s) that contain the actions you want. The getActions
should contain the action names you want on the component (you can use either or both). The ActionCreator APIs will appear on the component as top level method names. Use the lux.reactMixin.actionCreator
with the React.createClass
mixins property.
The store
mixin (which is provided automatically when you call lux.createControllerView
) will look for a stores
property on your component options. This property should be an object with a listenTo
array containing the list of store namespaces your component wants state from, and an onChange
handler for when any of those stores publish state changes. You will need to include the store(s) as module dependencies to access their state from within the handler. Use the lux.reactMixin.store
with the React.createClass
mixins property.
The actionListener
mixin wires an instance into the message bus to listen for execute.*
messages on the lux.action
channel. The subscription handler looks for a handlers
property on the instance it's mixed into, and will fire any method that matches the action type passed on the message envelope. This is useful for integration non-lux modules into lux operations. For example, you may have a "remote data" (i.e. - HTTP/websockets) wrapper that uses this mixin to listen for actions that indicate an AJAX request needs to be made, etc.
The lux.mixin()
method allows you to add support for lux stores and actions into any object. You will not need to use this on React components or existing lux components. Simply call lux.mixin( this )
on your object, and lux will do the following steps:
- If it finds a
stores
property on your object it will wire up stores and theonChange
handler. - If it finds a
getActionGroup
orgetActions
property on your object it will add top level methods for the needed actions. - It will add a
luxCleanup()
method to your object. Invoke this method during a destroy or teardown lifecycle event to cleanup the mixin's subscriptions.
You can also specify the lux mixins you wish to use by calling lux.mixin(taget, lux.mixin.actionListener, lux.mixin.store);
.
Lux contains some helper methods that enable you to do the following:
In this example, we have an API wrapper that is a lux action listener, so that it will get notified (via messaging) when the loadBoard action is dispatched:
var http = lux.actionListener({
handlers: {
loadBoard: function(boardId) {
// simulate an http response
setTimeout(function(){
var board = mockData.boards.find(function(x) {
return x.boardId.toString() === boardId.toString();
});
lux.publishAction("boardLoaded", boardId, board);
}.bind(this), 200);
}
}
});
In this example, we're creating an instance of a plain object (not a lux component or controller view) that will have any actions associated with the "board" action group:
var boardActions = lux.actionCreator({
getActionGroup: [ "board" ]
});
// assuming the board action group has a "toggleLaneSelection" action
boardAction.toggleLaneSelection(123, 4567);
If you just need to publish an action, you can do that by calling lux.publishAction( actionName, arg1, arg2... )
. Just a reminder: even though you can use this anywhere in your application, it is strongly discouraged (i.e. - we think it's an anti-pattern) to ever have a store publish actions.
lux.publishAction( "initializePage" ); // kick off page setup
lux currently has two console utility methods:
lux.utils.printStoreDepTree()
- prints (in Chrome) a table of each action, and the dependency tree of participating stores, showing them in the order they will handle the action.lux.utils.printActions()
- prints (in Chrome) a table of the known actions.
Some flux implementations handle remote data access inside stores. We believe that the best way to handle this kind of I/O is outside a store.
In addition to tightly coupling stores to a transport, making HTTP calls from a store handler requires action dispatch cycles to be asynchronous, and typically results in stores publishing actions of their own when responses have been recieved. This scenario undermines the benefits gained from a clean uni-directional data flow and removes the predictable nature of knowing where state changes originated from.
In our apps, we have an API wrapper module (it could wrap $.ajax
, a WebSocket lib, or whatever you use - we usually wrap halon). This API module uses the lux.mixin.actionCreator
and lux.mixin.actionListener
mixins so that it listens for actions (much like the Dispatcher does), and it can also publish actions (when a response is received, for example). For example:
var api = lux.actionCreatorListener( {
handlers: {
cartCheckout: function( products ) {
shop.buyProducts( products, function() {
this.publishAction( "successCheckout", products );
}.bind( this ) );
},
getAllProducts: function() {
shop.getProducts( function( products ) {
this.publishAction( "receiveProducts", products );
}.bind( this ) );
},
}
} );
The above API wrapper listens for cartCheckout
and getAllProducts
actions, and is capable of publishing successCheckout
and receiveProducts
actions. Check out the "Examples and More" section at the bottom if you're interested in seeing more.
- A Store implementation that utilizes immutable.js
- An API wrapper for things like HTTP/websockets, etc.
- Debug helpers/add-ons to enhance dev-time visibility
- postal
- machina
- babel polyfill lux is written in ES6 and then transpiled to ES5, so you need to include the babel polyfill either in your build, or on your page(s) before lux is loaded. (The polyfill is necessary because lux uses generator functions.) babel is a peer dependency of lux.
NOTE: If you're using bower, you will need to grab the babel polyfill and include it manually. If you're using npm, you will need to
npm install babel
in your project.
- ReactJS – The
lux.reactMixin
mixins are made to work with React. Lux only needs a reference to React if you plan on usinglux.component
andlux.controllerView
methods. In that case, callinglux.initReact( React )
will allow those methods to work with your version of React.
- navigate to root of project and run
npm install
- type
gulp
in the console. - built files appear under
/lib
in the project root.
###To Run Tests:
npm test
runs a console test suitenpm run coverage
runs istanbul coverage reporter in console (and generates an .html report)npm run show-coverage
opens the above coverage report in your browser (if you're on a Mac)
- Doug Neiner has created a great example app here.
- Check out the flux-comparison project so you can see what lux looks like compared to other great flux implementations.