Awesome
Project status
It's unfinished and frozen for now because of existence of a better alternative — Redux
Fluce
Well, Flux again ...
- lib agnostic, but with helpers for React
- forces to use immutable data structures in stores, or just never mutate
- forces to use pure functions in stores
- stores are just reducers of actions to their state
- server-side ready
- without singletons
The name is combined from "flux" and "reduce".
Installation
$ npm install fluce
Store
Store in Fluce is just an object with the following shape:
{
initial: Function,
reducers: {
foo: Function,
bar: Function,
...
}
}
Where initial()
returns an initial state, and each of reducers
is an action
handler called with the current state and the action's payload as arguments
returning a new state. Each reducer must be a pure function, that never
mutate current state, but returns a new one instead. A reducer's name
(e.g. foo
above) is the action type that the reducer want to handle.
let myStore = {
initial() {
return myInitialState;
},
reducers: {
actionType1(currentStoreState, actionPayload) {
return computeNewState(currentStoreState, actionPayload);
},
actionType2(currentStoreState, actionPayload) {
/* ... */
}
}
};
Fluce instance
You start use Fluce by creating an instance of it. Normally you want only one instance in the browser, but may want to create an instance for each request on the server.
let createFluce = require('fluce/create-fluce');
let fluce = createFluce();
When an instance is created, you can add stores to it:
fluce.addStore('storeName1', myStore1);
fluce.addStore('storeName2', myStore2);
After this is done, you can access each store's current state as fluce.stores.storeName
,
and dispatch actions with fluce.dispatch('actionType', payload)
.
Also you can subscribe to changes of stores' states:
let unsubscribe = fluce.subscribe(['storeName1', 'storeName2'], (updatedStoresNames) => {
// you can read new state directly from `fluce.stores.storeName`
});
// later...
unsubscribe();
Callback is called when some of specified stores change their state (via a reducer). If two or more stores change in response to a single action, the callback will be called only once. Also if a reducer returns same state, the store will be considered not changed.
Example
let createFluce = require('fluce/create-fluce');
// Setup
let fluce = createFluce();
fluce.addStore('counter', {
initial() {
return 0;
},
reducers: {
counterAdd(cur, x) {
return cur + x;
},
counterSubtract(cur, x) {
return cur - x;
}
}
});
fluce.addStore('counterInverted', {
initial() {
return 0;
},
reducers: {
counterAdd(cur, x) {
return cur - x;
},
counterSubtract(cur, x) {
return cur + x;
}
}
});
// Usage
console.log(fluce.stores.counter); // => 0
console.log(fluce.stores.counterInverted); // => 0
fluce.actions.dispatch('counterAdd', 10);
console.log(fluce.stores.counter); // => 10
console.log(fluce.stores.counterInverted); // => -10
fluce.subscribe(['counter', 'counterInverted'], (updated) => {
console.log('following stores have updated:', updated);
});
fluce.actions.dispatch('counterSubtract', 5);
// => following stores have updated: ['counter', 'counterInverted']
In progress
The features below aren't done yet.
<Fluce /> React component
<Fluce/>
is an helper component, you can use to subscribe to stores and
fire actions from a component that know nothing about Fluce. It outputs nothing
but it's child component to the result DOM. It can have only one child, and
renders it with a bit of a magic (adds more props to it).
<Fluce/>
accepts following props:
fluce
— the Fluce instance to use, the property is optional if there is another<Fluce/>
up the tree with this property specified.stores
— object of the shape{foo: 'storeName', bar: 'anotherStoreName', ...}
, containing name of stores from which you want to read. Current state of each of these stores will be always available on the child component's props (foo
andbar
are the props names).actionCreators
— object of shape{foo: (fluce, arg1, agr2) => {...}, ...}
, containing action creators that will be available as props on the child component. You will be able to call them like thisthis.props.foo(arg1, agr2)
(without providing the first argumentfluce
— an instance of Fluce).render
— a custom render function you can provide, that will be used instead of simply render child with additional props.
let Fluce = require('fluce/fluce-component');
class Counter extends React.Component {
render() {
// Also `this.props.fluce` will be available here,
// but you shouldn't need it in most cases.
return <div>
<button onClick={this.props.onDecrement}>-</button>
{this.props.counter}
<button onClick={this.props.onIncrement}>+</button>
</div>;
}
}
function increment(fluce) {
fluce.dispatch('counterAdd', 1);
}
function decrement(fluce) {
fluce.dispatch('counterSubtract', 1);
}
class App extends React.Component {
constructor() {
render() {
return <div>
...
<Fluce
stores={{counter: 'myCounterStore'}}
actionCreators={{onDecrement: decrement, onIncrement: increment}}
>
<Counter />
</Fluce>
...
</div>;
}
}
}
React.render(<Fluce fluce={fluce}><App /></Fluce>, document.getElementById('root'));
And here is an example with custom render function:
<Fluce
stores={{counter: 'myCounterStore'}}
actionCreators={{decrement, increment}}
render={(stores, actionCreators, fluce) => {
return <Counter
counter={stores.counter}
onIncrement={actionCreators.increment}
onDecrement={actionCreators.decrement}
/>;
}}
/>
Internally we use context
to pass fluce
instance through components tree, and
React.addons.cloneWithProps
to add props to child component.
Optimistic dispatch
Thanks to pure action handlers we can support optimistic dispatch of actions. An optimistic dispatch can be canceled, in this case we simply roll back to the state before that action, and replay all actions except the canceled one.
fluce.addActionCreator('fooAdd', (fluce) => {
return (foo) => {
let action = fluce.optimisticallyDispatch('fooAdd', foo);
addFooOnServer(foo)
.then(
// To confirm an optimistic dispatch is as important as to cancel,
// because before it confirmed we have to collect
// all actions (with payloads) that comes after the action in question.
() => action.confirm(),
() => action.cancel()
);
};
});