Skip to content
/ fluo Public
forked from reflux/refluxjs

A prototype-based fork of the Reflux data flow library similar to Facebook Flux.

License

Notifications You must be signed in to change notification settings

jankuca/fluo

 
 

Repository files navigation

Fluo

A prototype-based fork of the Reflux data flow library similar to Facebook Flux.

To see what is different in this fork from Reflux, see the release notes and changelog of 1.0.x


NPM Version NPM Downloads Bower Version Build Status

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.

Content

Install/Download

The latest release is always downloadable from GitHub: jankuca/fluo/releases.

NPM

The following command installs Fluo as an npm package:

npm install fluo

Bower

The following command installs Fluo as a bower component that can be used in the browser:

bower install fluo

ES5

Like React, Fluo depends on an ES5-shim for older browsers. Kriskowal's es5-shim provides everything required.

Back to top

Comparing Fluo with Facebook Flux

The goal of the Fluo 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:

You can read more in this blog post about React Flux vs Reflux (Fluo).

Similarities with Flux

Some concepts are still in Fluo in comparison with Flux:

  • There are actions
  • There are data stores
  • The data flow is unidirectional

Differences with Flux

Fluo has refactored Flux to be a bit more dynamic and be more Functional Reactive Programming (FRP) friendly:

  • The singleton dispatcher is removed in favor for letting every action act as dispatcher instead.
  • Because actions are listenable, the stores may listen to them. Stores don't need to have big switch statements that do static type checking (of action types) with strings
  • Stores may listen to other stores, i.e. it is possible to create stores that can aggregate data further, similar to a map/reduce.
  • waitFor() is replaced in favor to handle serial and parallel data flows:
  • Aggregate data stores (mentioned above) may listen to other stores in serial
  • Joins for joining listeners in parallel
  • Action creators are not needed because Fluo actions are functions that will pass on the payload they receive to anyone listening to them

Back to top

Examples

Here is an example application built upon Fluo: jankuca/fluo-example

You can find some more example projects at the following locations. These are built upon Reflux though.

Back to top

Usage

For a full example check the test/index.js file.

Back to top

Creating actions

Create an action by calling fluo.createAction() with an optional options object.

var statusUpdate = fluo.createAction(options);

An action is a functor that can be invoked like any function.

statusUpdate(data); // Invokes the action statusUpdate
statusUpdate.trigger(data); // same effect as above

If options.sync is true, the functor will instead call action.triggerSync() which is a synchronous operation.

There is also a convenience function for creating multiple actions.

var actions = fluo.createActions([
    'statusUpdate',
    'statusEdited',
    'statusAdded'
]);

// The actions object now contains the actions
// with the names given in the array above
// that may be invoked as usual

actions.statusUpdate();

Asynchronous actions

For actions that represent asynchronous operations (e.g. API calls), a few separate dataflows result from the operation. In the most typical case, we consider completion and failure of the operation. To create related actions for these dataflows, which you can then access as attributes, use options.children.

// this creates 'load', 'load.completed' and 'load.failed'
var actions = fluo.createActions({
    'load': { children: [ 'completed', 'failed'] }
});

// when 'load' is triggered, call async operation and trigger related actions
actions.load.listen(function () {
    // By default, the listener is bound to the action
    // so we can access child actions using 'this'
    someAsyncOperation()
        .then(this.completed)
        .catch(this.failed);
});

There is a shorthand to define the completed and failed actions in the typical case: options.asyncResult. The following are equivalent:

createAction({
    children: [ 'progressed', 'completed', 'failed' ]
});

createAction({
    asyncResult: true,
    children: [ 'progressed' ]
});

There are a couple of helper methods available to trigger the completed and failed actions:

  • #promise(promise) - Expects a promise object and binds the triggers of the completed and failed child actions to that promise, using then() and catch().

  • #listen(callback) - Expects a function which can return a promise object. If it does, #promise() is called with the returned promise object.

Therefore, the following are all equivalent:

asyncResultAction.listen(function (arguments) {
    someAsyncOperation(arguments)
        .then(asyncResultAction.completed)
        .catch(asyncResultAction.failed);
});

asyncResultAction.listen(function (arguments) {
    asyncResultAction.promise(someAsyncOperation(arguments));
});

asyncResultAction.listen(someAsyncOperation);
Asynchronous actions as Promises

Asynchronous actions can be used as promises, which is particularly useful for server-side rendering when you must await the successful (or failed) completion of an action before rendering.

Suppose you had an action + store to make an API request:

// Create async action with `completed` & `failed` children
var makeRequest = fluo.createAction({ asyncResult: true });

var RequestStore = fluo.createStore({
    init: function () {
        this.listenTo(makeRequest, 'onMakeRequest');
    },

    onMakeRequest: function (url) {
        // Assume `request` is some HTTP library (e.g. superagent)
        request(url, function (response) {
            if (response.ok) {
                makeRequest.completed(response.body);
            } else {
                makeRequest.failed(response.error);
            }
        })
    }
});

Then, on the server, you could use promises to make the request and either render or serve an error:

makeRequest.triggerPromise('/api/something').then(function (body) {
    // Render the response body
}).catch(function (err) {
    // Handle the API error object
});

Action hooks

There are a couple of hooks available for each action.

  • preEmit() - Is called before the action emits an event. It receives the arguments from the action invocation. If it returns something other than undefined, that will be used as arguments for shouldEmit() and subsequent emission.

  • 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);
// Should output: 1

You can also set the hooks by sending them in a definition object as you create the action:

var action = fluo.createAction({
    preEmit: function () { /* ... */ },
    shouldEmit: function () { /* ... */ }
});

fluo.Action

If you would like to have a common set of methods available to all actions you can extend the fluo.Action object and create instances the standard way – new MyAction(definition).

Example usage:

var MyAction = function () {
    return fluo.Action.apply(this, arguments);
};
MyAction.prototype = Object.create(fluo.Action.prototype);

MyAction.prototype.exampleMethod = function () {
    console.log(arguments);
};

Notice the return keyword in the constructor. This is required for created actions to be functors (function objects) so that you can simply call action() instead of action.trigger().

Back to top

Creating data stores

The recommended way to create stores is by extending the provided fluo.Store prototype.

var StatusStore = function () {
    fluo.Store.call(this);
};
StatusStore.prototype = Object.create(fluo.Store.prototype);

StatusStore.prototype.init = function () {
    this.listenTo(statusUpdate, this.output);
};

StatusStore.prototype.output = function () {
    var status = flag ? 'ONLINE' : 'OFFLINE';
    this.trigger(status);
};

var statusStore = new StatusStore();

Deprecated: You can also create a data store much like React components (react.createClass()) by passing a definition object to fluo.createStore(). You may set up all action listeners in the init() function and register them by calling the store's own listenTo() function.

// Creates a Store instance
var statusStore = fluo.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() method call is true.

A data store is a publisher much like the actions, so they too have the preEmit() and shouldEmit() hooks.

Listening to many actions at once

Since it is a very common pattern to listen to all actions from a fluo.createActions() call in a store init() call, the store has a listenToMany() function that takes an object of listenables. Instead of doing this:

var actions = fluo.createActions([ 'fireBall', 'magicMissile' ]);

var store = fluo.createStore({
    init: function () {
        this.listenTo(actions.fireBall, this.onFireBall);
        this.listenTo(actions.magicMissile, this.onMagicMissile);
    },
    onFireBall: function () {
        // whoooosh!
    },
    onMagicMissile: function () {
        // bzzzzapp!
    }
});

...you can do this:

var actions = fluo.createActions([ 'fireBall', 'magicMissile' ]);

var store = fluo.createStore({
    init: function () {
        this.listenToMany(actions);
    },
    onFireBall: function () {
        // whoooosh!
    },
    onMagicMissile: function () {
        // bzzzzapp!
    }
});

This will add listeners to all actions actionName who have a corresponding onActionName() (or actionName if you prefer) method in the store. Thus if the actions object should also have included an iceShard spell, that would simply be ignored.

The listenables shorthand

To make things more convenient still, if you give an object of actions to the listenables property of the store definition, that will be automatically passed to listenToMany(). So the above example can be simplified even further:

var actions = fluo.createActions([ 'fireBall', 'magicMissile' ]);

var store = fluo.createStore({
    listenables: actions,
    onFireBall: function () {
        // whoooosh!
    },
    onMagicMissile: function () {
        // bzzzzapp!
    }
});

The listenables property can also be an array of such objects, in which case all of them will be sent to listenToMany(). This allows you to do convenient things like this:

var Store = fluo.createStore({
    listenables: [
        require('./darkspells'),
        require('./lightspells'),
        { healthChange: require('./healthstore') }
    ],
    // rest redacted
});

Listening to changes in data store

In your component, register to listen to changes in your data store like this:

// Fairly simple view component that outputs to console
function ConsoleComponent() {
    // Registers a console logging callback to the statusStore updates
    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

Back to top

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 () {
        // render specifics
    }
});

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 fluo.ListenerMixin. Using that, the above example can be written like thus:

var Status = react.createClass({
    mixins: [ fluo.ListenerMixin ],
    onStatusChange: function (status) {
        this.setState({
            currentStatus: status
        });
    },
    componentDidMount: function () {
        this.listenTo(statusStore, this.onStatusChange);
    },
    render: function () {
        // render specifics
    }
});

The mixin provides the listenTo() method for the React component, that works much like the one found in the Fluo's stores, and handles the listeners during mount and unmount for you. You also get the same listenToMany() method as the store has.

Using fluo.listenTo()

If you're not reliant on any special logic for the this.listenTo() calls inside componentDidMount(), you can instead use a call to fluo.listenTo() as a mixin. That will automatically set up the componentDidMount() and the rest for you, as well as add the ListenerMixin functionality. With this our example above can be reduced even further:

var Status = react.createClass({
    mixins: [ fluo.listenTo(statusStore, 'onStatusChange') ],
    onStatusChange: function (status) {
        this.setState({
            currentStatus: status
        });
    },
    render: function () {
        // render using `this.state.currentStatus`
    }
});

You can have multiple calls to fluo.listenTo() in the same mixins array.

There is also fluo.listenToMany() which works in exactly the same way, exposing listener.listenToMany().

Using fluo.connect()

If all you want to do is update the state of your component to whatever the data store transmits, you can use fluo.connect(listener, [stateKey]) as a mixin. If you supply a stateKey the state will be updated through this.setState({ <stateKey>: data }), otherwise this.setState(data). Here's the example above changed to use this syntax:

var Status = react.createClass({
    mixins: [ fluo.connect(statusStore, 'currentStatus') ],
    render: function () {
        // render using `this.state.currentStatus`
    }
});

Using fluo.connectFilter

fluo.connectFilter() is used in a similar manner to fluo.connect(). Use the connectFilter() mixin when you want only a subset of the items in a store. A blog written using Fluo would probably have a store with all posts in it. For an individual post page, you could use fluo.connectFilter() to filter the posts to the post that's being viewed.

var PostView = react.createClass({
    mixins: [ fluo.connectFilter(postStore, 'post', function (posts) {
        return posts.filter(function (post) {
           return (post.id === this.props.id);
        }.bind(this))[0];
    }) ],
    render: function () {
        // render using `this.state.post`
    }
});

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:

// Creates a Store that listens to statusStore
var statusHistoryStore = fluo.createStore({
    init: function () {
        // Register statusStore's changes
        this.listenTo(statusStore, this.output);
        this.history = [];
    },

    // Callback
    output: function (statusString) {
        this.history.push({
            date: new Date(),
            status: statusString
        });
        // Pass the data on to listeners
        this.trigger(this.history);
    }
});

Back to top

Advanced usage

Switching EventEmitter

Don't like to use the EventEmitter provided? You can switch to another one, such as node.js's own like this:

// Do this before creating actions or stores
fluo.setEventEmitter(require('events').EventEmitter);

Switching Promise library

Don't like to use the Promise library provided? You can switch to another one, such as Bluebird like this:

// Do this before triggering actions
fluo.setPromise(require('bluebird'));

Note that promises are constructed with new Promise(...). If your Promise library uses factories (e.g. Q), then use fluo.setPromiseFactory() instead.

Switching Promise factory

Since most Promise libraries use constructors (e.g. new Promise(...)), this is the default behavior.

However, if you use Q or another library that uses a factory method, you can use fluo.setPromiseFactory() for it.

// Do this before triggering actions
fluo.setPromiseFactory(require('Q').Promise);

Switching nextTick()

Whenever action functors are called (except via Action#triggerSync()), they return immediately through the use of setTimeout() (nextTick() function) internally.

You may switch out for your favorite setTimeout(), nextTick(), setImmediate(), et al implementation:

// node.js env
fluo.nextTick(process.nextTick);

For better alternative to setTimeout(), you may opt to use the setImmediate() polyfill, setImmediate2 or macrotask.

Joining parallel listeners with composed listenables

The Fluo API contains join*() methods that makes it easy to aggregate publishers that emit events in parallel. This corresponds with the waitFor() mechanism in Flux.

Argument tracking

A join is triggered once all participating publishers have emitted at least once. The callback will be called with the data from the various emissions, in the same order as the publishers were listed when the join was created.

There are four join methods, each representing a different strategy to track the emission data:

  • joinLeading(): Only the first emission from each publisher is saved. Subsequent emissions by the same publisher before all others are finished are ignored.
  • joinTrailing(): If a publisher triggers twice, the second emission overwrites the first.
  • joinConcat(): An array of emission arguments are stored for each publisher.
  • joinStrict(): An error is thrown if a publisher emits twice before the join is completed.

The method signatures all look like this:

join*(...publisher, callback)

Once a join is triggered, it will reset, and thus it can trigger again when all publishers have emitted anew.

Using the listener instance methods

All objects using the listener API (stores, React components using ListenerMixin, or other components using the ListenerMethods) gain access to the four join instance methods, named after the argument strategy. Here's an example saving the last emission from each publisher:

var gainHeroBadgeStore = fluo.createStore({
    init: function () {
        this.joinTrailing(
            actions.disarmBomb,
            actions.saveHostage,
            actions.recoverData,
            this.trigger
        );
    }
});

actions.disarmBomb('warehouse');
actions.recoverData('seedyletter');
actions.disarmBomb('docks');
actions.saveHostage('offices', 3);
// `gainHeroBadgeStore` will now asyncronously trigger `[[ 'docks' ], [ 'offices', 3 ], [ 'seedyletter' ]]`.

Using the static methods

Since it is rather common to have a store where the only purpose is to listen to a join and trigger when the join is completed, the join methods have static counterparts on the fluo object which return stores listening to the requested join. Using them, the store in the example above could instead be created like this:

var gainHeroBadgeStore = fluo.joinTrailing(
    actions.disarmBomb,
    actions.saveHostage,
    actions.recoverData
);

Sending initial state 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 getInitialState() is returning.

var exampleStore = fluo.createStore({
    init: function () {},
    getInitialState: function () {
        return 'the initial data';
    }
});

// Anything that will listen to the example store
this.listenTo(exampleStore, onChangeCallback, initialCallback)

// initialCallback will be invoked immediately with 'the initial data' as the first argument

Remember the listenToMany() method? In case you use that with other stores, it supports getInitialState(). That data is sent to the normal listening callback, or a this.on<Listenablename>Default() method if that exists.

Back to top

Colophon

List of contributors is available on Github.

This project is licensed under BSD 3-Clause License. Copyright (c) 2014, Mikael Brassman, Jan Kuča.

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.

About

A prototype-based fork of the Reflux data flow library similar to Facebook Flux.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • JavaScript 100.0%