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
- Each action creators module should contain an actions class that extends
Fluent.Actions
, and defines its action creators as class methods. - Each action creator method often calls
this.dispatch(...)
to dispatch the action to interested stores. - 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
- 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
.
- a view action handlers object
- About the action handler:
- The function name of the action handler in
viewActionHandlers
orserverActionHandlers
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.)
- The function name of the action handler in
- 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
- Each controller-view module should contain a controller-view class that extends
React.Component
. - 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)
- parameter #1: the
(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:
- Define a server action creator
init(payload)
. The payload object contains all the data the stores need. - 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. - Server side:
- Call the server action creator
init(payload)
, then callrenderToString
. - In addition to the HTML string, also output
<script id="setup" type="application/json">${jsonString}</script>
wherejsonString = JSON.stringify(payload)
.
- Call the server action creator
- Client side:
- Call the server action creator
init(setup)
wheresetup = JSON.parse(document.getElementById('setup').innerHTML)
. - Call
ReactDOM.render
(orReact.render
with older React).
- Call the server action creator
Some Flux Tips
We follow these guidelines during our React/Flux development:
-
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.
-
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.
-
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).
-
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.