The Flux pattern is really interesting because in its essence it is fully reactive. I think it perfectly matches the Rx way of doing things and combined with rx-react
, rx-flux
could result in a solid architecture.
However, I have some problems to figure the right API for rx-flux
. I want to publish this project on npm but, before that, I want to create an API that will provide this project users simplicity, elegance, and freedom.
A good library is hard to design. Despite the fact that it is its role to constraint the user to develop in a certain way, to reduce boilerplate and transform the hard into simple, each feature that you offer makes that library less usable for people who don't want to use those features.
Sometimes it's better to let the user build their own abstractions on top of your library than trying to solve every use case.
The only thing I'm pretty happy with actually in rx-flux
is the store object. When you think about what is a Store in the Flux pattern, the answer is pretty simple : a reduce
operation.
At a given instant, the value held by the store is the result of a reduce
operation over the list of all actions that have been dispatched.
That perspective make things easy to reason about. For example, if your store state is erroneous it's easy to understand where the problem comes from by recording/replaying action.rx-flux
stores mimics exactly that model: a seed value and a set of transformations for each action dispatched.
var TodoStore = Store.create({
getInitalValue: () => ({}),
getOperations: () =>
Rx.Observable.merge(
todoCreated.map(todo => ({
transform: (todos) => ({ ...todos, [todo.id]: todo })
})),
....
)
}
Singleton Dispatcher vs Independent Actions
I tried to reduce all the boilerplate code of the original Flux pattern by creating function-object ala reflux and getting rid of the central dispatcher.
After experimenting a bit with it, I dislike the result.
An rx-flux
action has too much responsibility; it is the constant, the action creator and the dispatcher at the same time. Worse, that pattern does not provide the single source of truth that is the central dispatcher in the original Flux pattern.
Unless someone completely disagrees and proves that I should not do so, I'm seriously considering getting rid of those *function-object * and reintroducing a global singleton dispatcher.
Sure it is a little more verbose, but the single fact that you can record/replay all the actions from a single source worth the deal in my opinion.
There is something that I dislike and want to replace in the original Flux pattern: ActionCreator.
In my point of view, ActionCreator is the garbage of the original Flux pattern where everything that breaks the unidirectional data flow has been put.
An ActionCreator validates the input and creates the action object - no surprise here - but it also dispatches the action and acts as command by performing an async task and dispatching another action depending on the result.
Now I still think that there is a need of a simple function that creates an action and validates the input but it should not dispatch the created action itself nor it should perform any other tasks.
Instead, I want to let the component dispatch itself the value returned by the action creator and let the WebService
layer listens dispatched actions and performs async tasks.
So here is the problem that I want to solve from the start and that gives me so much headache:
When an action is associated to an async task, if that task fails, the store may have to revert its state.
Simple case are obvious but this can create a lot of complexity.
For that purpose, an operation consumed by an rx-flux
store can provide a promise as a confirmation token :
todoCreated.map(todo => ({
transform: (todos) => ({ ...todos, [todo.id]: todo }),
confirm: aPromise
}))
When the promise is resolved, the operation is confirmed and in the contrary, if the promise is rejected, the store state is reverted.
Now the problem is: how we generate that promise.
In the current rx-flux version, I have something similar to the ActionCreator so it is more or less easy. The ActionCreator performs the async task and the promise is just a property of the action object.
However, that strategy causes more problems that it solves.
Firstly, as I said, I don't want the ActionCreator to be responsible of executing the async task. Also, I think that the action object should be a serializable object. Finally some stores can be interested in that async task failure/success so the result of that task should also dispatch an action.
The original Flux pattern promotes 3 constants per action. The first one for the original action, one for failure and one for success. This is a bit verbose but more importantly, it does not provide a way to see how the result/error actions are related to the original one.
Reflux has some sort of async
actions but does not really promote a way of resolving things.
Here are the different proposals I have thought about. For each one of this, I'll show a little example of a todo creation.
Simple Dispatcher withgetAsyncToken
In this proposal, each action has an unique id generated by the Dispatcher; that helps creating the relation between success/failure but it produces a lot of boilerplate code.
// AppDispatcher.js // The app dispatcher pretty similar to the original flux one, // except the registration/listening process That is based on rxjs var Dispatcher = require('rx-flux').Dispatcher; module.exports = Dispatcher.create(); //TodoConstants.js var keyMirror = require('keymirror'); //Three constant by *async* action, one for the action one for failure one for success module.exports = keyMirror({ TODO_CREATE: null, TODO_CREATE_SUCCESS: null, TODO_CREATE_FAILURE: null }); //TodoActions.js var TodoContants = require('./TodoContants'); var uuid = require('uuid'); // the action creator, responsible for creating the action object and validating the input module.exports = { todoCreate(text) { if (!text) { throw new Error('text should not be empty'); } return { type: TodoContants.TODO_CREATE, data: { id: uuid(), text: text, completed: false } } }, todoCreateSuccess(action, result) { return { type: TodoContants.TODO_CREATE_SUCCESS, parentAction: action.id // the action is related to the original action through the *id* data: result } } todoCreateFailure(action, error) { return { type: TodoContants.TODO_CREATE_FAILURE, parentAction: action.id // the action is related to the original avtion through the *actionid* data: error } } } //TodoCommands.js var TodoConstants = require('./TodoConstants'); var TodoActions = require('./TodoActions'); var AppDispatcher = require('./AppDispatcher'); var WebApi = require(...); // Commands are responsible of executing async task AppDispatcher .getObservable(TodoContants.TODO_CREATE) .subscribe(function (action) { var todo = action.data; WebApi.createTodo(todo).then(function (result) { //on success we dispatch a *child action* AppDispatcher.dispatch(TodoActions.todoCreateSuccess(action, result)) }, function (error) { //on failure we dispatch a *child action* AppDispatcher.dispatch(TodoActions.todoCreateFailure(action, error)) }) }) //TodoStore.js var Store = require('rx-flux').Store; var TodoConstants = require('./TodoConstants'); var AppDispatcher = require('./AppDispatcher'); var todoCreated = ( AppDispatcher .getObservable(TodoContants.TODO_CREATE) // create an observable that filter by action type .map(todo => ({ transform: (todos) => ({ ...todos, [todo.id]: todo }), // a promise resolved when a *child* action with the constants is dispatched confirm: AppDispatcher.getAsyncToken( TodoContants.TODO_CREATE_SUCCESS, TodoContants.TODO_CREATE_FAILURE, action.id ) })), ) var TodoStore = Store.create({ getInitalValue: () => ({ }), getOperations: () => Rx.Observable.merge(todoCreated) } //component dispatching ... createTodo(event) { var text = event.target.value; AppDispatcher.dispatch(TodoActions.createTodo(text)); // the component is responsible for dispatching } ...Dispatcher with integrated
command
Now in the second proposal, the Dispatcher provides a 'command' mechanism while this reduces boilerplate code and constraints the usage. I'm not so confident it provides the necessary freedom.
For this proposal, I'll provide the Dispatcher api (typescript/flow notations).
interface Dispatcher { /** * Produce an observable that notify of dispatched action, * If type is provided action are filtered by this type */ getObservable(type?: string): Rx.Observable /** * Dispatch an action */ dispatch(action: {type: string, data: any}) /** * Register a command, a command is a function that return a Promise */ registerCommand(type?: string, command: (action) => Promise<any>); /** * observable exposing 'command' results */ commandResult(type: string): Rx.Observable /** * observable exposing 'command' errors */ commandError(type: string): Rx.Observable; /** * observable exposing 'command' completion */ commandComplete(type: string): Rx.Observable; /** * observable exposing async task status */ commandStatus(type: string): Rx.Observable<boolean>; /** * return a promise that resolve with the result of commands associated to the given action type */ getAsyncToken(action: { type: data}): Promise } ```javascript // AppDispatcher.js // The app dispatcher pretty similar to the original flux one, // except the registration/listening process That is based on rxjs // and the provided command system var Dispatcher = require('rx-flux').Dispatcher; module.exports = Dispatcher.create(); //TodoConstants.js var keyMirror = require('keymirror'); // only one constant for async action, other constant are generated by the Dispatcher, // in this case TODO_CREATE:result TODO_CREATE:error module.exports = keyMirror({ TODO_CREATE: null }); //TodoActions.js var TodoConstants = require('./TodoConstants'); var uuid = require('uuid'); // the action creator, responsible for creating the action object and validating the input module.exports = { todoCreate(text) { if (!text) { throw new Error('text should not be empty'); } return { type: TodoConstants.TODO_CREATE, data: { id: uuid(), text: text, completed: false } } } } //TodoCommands.js var TodoConstants = require('./TodoConstants'); var AppDispatcher = require('./AppDispatcher'); var WebApi = require(...); // Here we simply call the WebApi.createTodo that return a promise // Success/failure action are automaticly created/dispatched AppDispatcher.registerCommand(TodoConstants.TODO_CREATE, function ({data: todo}) { return WebApi.createTodo(todo) }) //TodoStore.js var Store = require('rx-flux').Store; var TodoConstants = require('./TodoConstants'); var AppDispatcher = require('./AppDispatcher'); var todoCreated = ( AppDispatcher .getObservable(TodoContants.TODO_CREATE) // creates an observable filtered by action type .map(todo => ({ transform: (todos) => ({ ...todos, [todo.id]: todo }), confirm: AppDispatcher.getAsyncToken(action) // a promise that resolves to whatever the command associated to the action resolves })), ) var TodoStore = Store.create({ getInitalValue: () => ({}), getOperations: () => Rx.Observable.merge(todoCreated) } //component dispatching ... createTodo(event) { var text = event.target.value; AppDispatcher.dispatch(TodoActions.createTodo(text)); // the component is responsible for dispatching } ...Conclusion
Flux is a solid pattern that makes our application state easy to reason about. So before publishing rx-flux
, I really want to grasp the right api. Any proposal/comment/opinion would be greatly appreciated.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4