FluentJS: A tiny Facebook Flux library (< 100 LOC)

React is a JavaScript library for creating user interfaces by Facebook and Instagram. Many people choose to think of React as the V in MVC. -- facebook.github.io/react

Flux is the application architecture that Facebook uses for building client-side web applications. It complements React's composable view components by utilizing a unidirectional data flow. It's more of a pattern rather than a formal framework... -- facebook.github.io/flux

The Flux code (or “pattern”) in Facebook GitHub repo is, however, too verbose to use in real projects, hence there are many 3rd-party Flux implementations.

FluentJS Features

FluentJS is a tiny Flux implementation we’re using in our site twincl.com. It has some good features:

  • Very tiny - less than 100 lines of code.
  • ES2015 class-based.
  • Can be used at server-side rendering. (For example, all twincl.com pages can be rendered at server side by appending a ?ssr=1 query parameter.)
  • Makes your code very terse. No more repetitive emitChange, add/removeChangeListener, ActionTypes constants, etc.
  • As close to Facebook Flux pattern and as few magic as possible.
  • Can be used with Facebook Flow type checker effectively. (For example, if you call an action creator or store method with wrong argument types, the Flow type checker will complain.)

If you already know Flux, a quick way to glimpse FluentJS is by looking at our fluent-chat example modified from the Facebook flux-chat code. This commit shows the code changes.

Usage

To install FluentJS, type:

npm install --save fluent-js

Dispatcher

FluentJS exposes Facebook Flux Dispatcher, so your project doesn’t have to require flux explicitly. Your dispatcher module should look like this:

var Dispatcher = require('fluent-js').Dispatcher;
module.exports = new Dispatcher();

or (in ES2015 module syntax)

import {Dispatcher} from 'fluent-js';
export default new Dispatcher();

Actions

  1. Each action creators module should contain an actions class that extends Fluent.Actions, and defines its action creators as class methods.
  2. Each action creator method often calls this.dispatch(...) to dispatch the action to interested stores.
  3. The action creators module usually exports an actions object instantiated from the actions class with 2 parameters:
    • parameter #1: the dispatcher
    • parameter #2 (optional): whether this is a server action class or not; true for server actions class, false (the default) for view actions

Example:

var Fluent = require('fluent-js');
var AppDispatcher = require('../dispatcher/AppDispatcher');

class MyViewActions extends Fluent.Actions {
  someAction(param1, param2) {
    this.dispatch(param1, param2);
    ...
  }
  anotherAction() {
    ...
  }
}

module.exports = new MyViewActions(AppDispatcher);

Stores

  1. Each store module should contain a store class that extends Fluent.Store, and an action handlers object that contains:
    • a view action handlers object viewActionHandlers, and/or
    • a server action handlers object serverActionHandlers.
  2. About the action handler:
    • The function name of the action handler in viewActionHandlers or serverActionHandlers should match the action creator name defined in the action creators module of which the store is interested.
    • The function parameters of the action handler should match the parameters of this.dispatch(...) call in the action creator, but not the parameters of the action creator itself (though they may be the same).
    • Its this is bound to the store object.
    • It may call this.waitFor([SomeStore, AnotherStore]) to wait for other stores to finish their action processing.
    • It may return false to suppress the change event emission. (By default, FluentJS automatically emits a change event to the listening controller-views upon each action handler’s return.)
  3. The store module usually exports a store object instantiated from the store class with 2 parameters:
    • parameter #1: the dispatcher
    • parameter #2: the action handlers object

Example:

class MyStore extends Fluent.Store {
   ...
}

var actionHandlers = {
  viewActionHandlers: {
    someAction(param1, param2) { ... }
    anotherAction() { ... }
  },
  serverActionHandlers: {
    someServerAction(param) { ... }
  }
};

module.exports = new MyStore(AppDispatcher, actionHandlers);

Controller-Views

  1. Each controller-view module should contain a controller-view class that extends React.Component.
  2. The controller-view module usually exports a React component class returned by Fluent.connectToStores that takes 4 parameters:
    • parameter #1: the React module
    • parameter #2: the controller-view class
    • parameter #3: an array of store modules
    • parameter #4 (optional): the attribute name of the onChange handler assigned in the controller-view constructor (if omitted, this.forceUpdate() will be called upon each store change event)

(The idea of Fluent.connectToStores was from Dan Abramov’s blog post.)

Example:

function getStateFromStores() {
  return { ... };
}

class MyControllerView extends React.Component {
  constructor(props) {
    super(props);
    this.state = getStateFromStores();
    this.onChange = this._onChange;
  }
  _onChange() { ... }
  render() { ... }
  ...
}

Server-Side Rendering

There are many ways to implement server-side rendering in FluentJS. The most important thing to keep in mind is that you should never make any asynchronous calls (e.g. web API or database calls) during store initialization or React components rendering. All asynchronous work should be done before you call action creators or renderToString. If you strictly follow this, you don’t need to use React contexts (or any contexts) at all.

Here is our approach:

  1. Define a server action creator init(payload). The payload object contains all the data the stores need.
  2. For each store, define a server action handler init(payload). It should be synchronous. Then the store takes what it needs from the payload to initialize its local data structures.
  3. Server side:
    • Call the server action creator init(payload), then call renderToString.
    • In addition to the HTML string, also output <script id="setup" type="application/json">${jsonString}</script> where jsonString = JSON.stringify(payload).
  4. Client side:
    • Call the server action creator init(setup) where setup = JSON.parse(document.getElementById('setup').innerHTML).
    • Call ReactDOM.render (or React.render with older React).

Some Flux Tips

We follow these guidelines during our React/Flux development:

  1. Stores should not expose setters.

    • Stores decide what to do by themselves upon the dispatched actions they are interested of.
    • Others may “suggest” changes through action creators, but cannot / should not manipulate store data directly.
  2. Separate controller-views from views clearly.

    • Controller-views: listen to store change events and get initial states from stores.
    • Views: belong to a view tree of which root is a controller-view, and may create view actions upon UI events.
  3. Separate server actions from view actions clearly.

    • View actions: triggered by view components (in UI event handlers, e.g. _onClick), and may further trigger server actions (in view action creators).
    • Server actions: triggered by web API callbacks (usually inside view action creators).
  4. Web API calls

    • Code related to web API calls should be consolidated in some web API utility library independent of the React/Flux code.
    • Each web API function takes two (optional) callbacks as parameters, one for on success and one for on failure.
    • Web APIs are usually called by view action creators in response to user actions. In web API callbacks, server action creators may be called for interested stores to process the web API results.

Conclusion

React and Flux are two great things from Facebook for web application development. Plus the Flow type checker, ESLint and ES2015 features, JavaScript programming has become much more pleasant than before.

We have been using FluentJS for several months, and think it is stable to share now. Hope this little library helps some people!

And ...

Shameless plug.

twincl.com is a new website characterized by forum, blog, Q&A and review sites. It’s built on top of React/Flux, producing very slim and responsive web pages. To get a free twincl.com trial account, please register with the following link (valid through 1/31):

https://twincl.com/account/register?pcode=HDJ12S

(If you post an article or a link by 1/31, the membership will be extended to 7/31.) Don’t hesitate to give it a try! Any suggestions or comments are welcome.

0 comment Comment