A Dart app architecture library with uni-directional data flow inspired by RefluxJS and Facebook's Flux.
w_flux
implements a uni-directional data flow pattern comprised of Actions
, Stores
, and FluxComponents
.
Actions
initiate mutation of app data that resides inStores
.- Data mutations within
Stores
trigger re-rendering of app view (defined inFluxComponents
). FluxComponents
dispatchActions
in response to user interaction.- and the cycle continues...
An Action
is a command that can be dispatched (with an optional data payload) and listened to.
In w_flux
, Actions
are the sole driver of application state change. FluxComponents
dispatch Actions
in response to
user interaction with the rendered view. Stores
listen for these Action
dispatches and mutate their internal data in
response, taking the Action
payload into account as appropriate.
import 'package:w_flux/w_flux.dart';
// define an action
final Action<String> displayString = new Action<String>();
// dispatch the action with a payload
displayString('somePayload');
// listen for action dispatches
displayString.listen(_displayAlert);
_displayAlert(String payload) {
print(payload);
}
BONUS: Actions
are await-able!
They return a Future that completes after all registered Action
listeners complete. It's NOT generally recommended to
use this feature within normal app code, but it is quite useful in unit test code.
A Store
is a repository and manager of app state. The base Store
class provided by w_flux
should be extended to fit
the needs of your app and its data. App state may be spread across many independent stores depending on the complexity
of the app and your desired app architecture.
By convention, a Store
's internal data cannot be mutated directly. Instead, Store
data is mutated internally in
response to Action
dispatches. Stores
should otherwise be considered read-only, publicly exposing relevant data ONLY
via getter methods. This limited data access ensures that the integrity of the uni-directional data flow is maintained.
A Store
can be listened to to receive external notification of its data mutations. Whenever the data within a Store
is mutated, the trigger
method is used to notify any registered listeners that updated data is available. In w_flux
,
FluxComponents
listen to Stores
, typically triggering re-rendering of UI elements based on the updated Store
data.
import 'package:w_flux/w_flux.dart';
class RandomColorStore extends Store {
// Public data is only available via getter method
String _backgroundColor = 'gray';
String get backgroundColor => _backgroundColor;
// Actions relevant to the store are passed in during instantiation
RandomColorActions _actions;
RandomColorStore(RandomColorActions this._actions) {
// listen for relevant action dispatches
_actions.changeBackgroundColor.listen(_changeBackgroundColor);
}
_changeBackgroundColor(_) {
// action dispatches trigger internal data mutations
_backgroundColor = '#' + (new Random().nextDouble() * 16777215).floor().toRadixString(16);
// trigger to notify external listeners that new data is available
trigger();
}
}
BONUS: Stores
can be initialized with a stream transformer to modify the standard behavior of the trigger
stream.
This can be useful for throttling UI rendering in response to high frequency Store
mutations.
import 'package:rate_limit/rate_limit.dart';
import 'package:w_flux/w_flux.dart';
class ThrottledStore extends Store {
...
ThrottledStore(this._actions) : super.withTransformer(new Throttler(const Duration(milliseconds: 30))) {
...
}
}
BONUS: Stores
provide an optional terse syntax for action -> data mutation -> trigger operations.
// verbose syntax
actions.incrementCounter.listen(_handleAction);
_handleAction(payload) {
// perform data mutation
counter += payload;
trigger();
}
// equivalent terse syntax
triggerOnAction(actions.incrementCounter, (payload) => counter += payload);
FluxComponents
define the (optional) user interface for a w_flux
unit and are responsible for rendering app view based
on 'Store' data as needed. FluxComponents
listen to Stores
and selectively re-render in response to their trigger
dispatches. FluxComponents
retrieve relevant app data from these Stores
via the exposed getter methods, and as such,
are internally stateless.
If user interaction with a FluxComponent
is intended to mutate app state, this is accomplished by dispatching an
Action
(with optional data payload). FluxComponents
DO NOT mutate app state within Stores
directly.
FluxComponent
is an extension of Component
(as provided by react-dart) that reduces the amount of boilerplate needed to
operate within the w_flux
architecture. The base FluxComponent
class provided by w_flux
should be extended to fit
the needs of your app.
By default, FluxComponents
provide standard getters for the Actions
and Store
that they are initialized with.
They also automatically subscribe to the provided Store
and re-render in response to its triggers
.
import 'package:react/react.dart' as react;
import 'package:w_flux/w_flux.dart';
var RandomColorComponent = react.registerComponent(() => new _RandomColorComponent());
class _RandomColorComponent extends FluxComponent<RandomColorActions, RandomColorStore> {
render() {
return react.div({
// accesses the backgroundColor via the store's public getter
'style': {'padding': '50px', 'backgroundColor': store.backgroundColor, 'color': 'white'}
}, [
'This module uses a flux pattern to change its background color.',
react.button({
'style': {'padding': '10px', 'margin': '10px'},
// triggers a change of background color by dispatching an action on button click
'onClick': actions.changeBackgroundColor
}, 'Change Background Color')
]);
}
}
BONUS: Optional overrides are available for more granular control of FluxComponent
rendering.
If the FluxComponent
's Store
is actually a complex object containing multiple Stores
(each trigger
independently),
the component's redrawOn
list can be overridden to confine re-rendering to trigger
dispatches that originate from
specific sub-stores.
The FluxComponent
's getStoreHandlers
method can be used to register more fine grained Store
trigger
handling if necessary.
import 'package:react/react.dart' as react;
import 'package:w_flux/w_flux.dart';
class ComplexStore {
ThisStore thisOne = new ThisStore();
ThatStore thatOne = new ThatStore();
OtherStore otherOne = new OtherStore();
}
var ComplexComponent = react.registerComponent(() => new _ComplexComponent());
class _ComplexComponent extends FluxComponent<ComplexActions, ComplexStore> {
// re-render will automatically be initiated in response to triggers from these two stores
// (e.g. no rendering will occur on store.otherOne triggers)
redrawOn() => [store.thisOne, store.thatOne];
// whenever store.otherOne triggers, the _handleOtherTrigger method will be executed
// (no rendering is triggered)
getStoreHandlers() => {store.otherOne: _handleOtherTrigger};
_handleOtherTrigger(otherStore) {
// decide whether to re-render based on some criteria
if (otherStore.isReady) {
// manually initiate re-render of this component
redraw();
}
}
render() {
...
}
}
If you are using the over_react
package
to build UI components, they offer a strongly-typed version of the "flux
component" pattern shown above.
https://github.com/workiva/over_react#flux-component-boilerplate
Simple examples of w_flux
usage can be found in the example
directory. The example README
includes instructions for building / running them.
w_flux
implements a uni-directional data flow within an isolated application or code module. If w_flux
is used as the
internal architecture of a library, this internal data flow should be considered when defining the external API.
- External API methods intended to mutate internal state should dispatch
Actions
, just like any internal user interaction. - External API methods intended to query internal state should leverage the existing read-only
Store
getter methods. - External API streams intended to notify the consumer about internal state changes should be dispatched from the
internal
Stores
, similar to theirtriggers
. - Factory constructors for useful 'root'
FluxComponents
can be exposed publicly for use in external react-dart based rendering hierarchies. These react components can be internally initialized with theActions
andStores
needed for normal operation without inadvertently exposing them externally.
w_module is a Dart library that defines a standard code module API that can be
used seamlessly with w_flux
internals to satisfy the above recommendations (complete with examples).
This project leverages the dart_dev package for most of its tooling needs, including static analysis, code formatting, running tests, collecting coverage, and serving examples. Check out the dart_dev readme for more information.