redux_machine 0.1.0 copy "redux_machine: ^0.1.0" to clipboard
redux_machine: ^0.1.0 copied to clipboard

outdatedDart 1 only

Redux State Machine and Store with no side-effects.

redux_machine #

Build Status Pub

Originally started to provide implementation of a State Machine using Redux design pattern, this library now includes its own Redux Store which can be used without the state machine part.

Important difference from other Redux implementations is in how side-effects are handled. ReduxMachine's opinion on this is simple - side-effects are not allowed in the action dispatch flow (dispatch-reduce-updateState).

Practical implications of this rule are:

  • reducers must be pure functions (no asynchronous logic)
  • no middleware (in the traditional form), middleware-like functionality is still allowed, as long as there is no side-effects.

Why? One of the main benefits of Redux pattern is how it reduces (no pun intended) cognitive load when modeling larger applications. Side-effects effectively remove this benefit.

ReduxMachine tries to avoid traditional middleware approach and keep side-effects out of the main action-reducer-state flow. It is not a new work and some inspiration has been taken from a few different online resources like this and this.

Provided APIs for StateMachine and Store classes are also designed to allow better static type analysis so you could catch errors earlier.

StateMachine Usage #

TL;DR see full source code of this example in the example/ folder.

Redux requires three things: state, actions and reducers.

We start by defining our state object. Here is an example of a coin-operated turnstile (from Wikipedia):

class Turnstile {
  final bool isLocked;
  final int coinsCollected;
  final int visitorsPassed;

  Turnstile(this.isLocked, this.coinsCollected, this.visitorsPassed);

  /// Convenience method to use in reducers.
  Turnstile copyWith({
    bool isLocked,
    int coinsCollected,
    int visitorsPassed,
  }) {
    return new Turnstile(
      isLocked ?? this.isLocked,
      coinsCollected ?? this.coinsCollected,
      visitorsPassed ?? this.visitorsPassed,
    );
  }
}

Next, actions:

abstract class Actions {
  /// Put coin to unlock turnstile
  static const ActionBuilder<Null> putCoin =
      const ActionBuilder<Null>('putCoin');
  /// Push turnstile to pass through
  static const ActionBuilder<Null> push = const ActionBuilder<Null>('push');
}

And reducers:

Turnstile putCoinReducer(
    Turnstile state, Action<Null> action, ActionDispatcher dispatcher) {
  int coinsCollected = state.coinsCollected + 1;
  print('Coins collected: $coinsCollected');
  return state.copyWith(isLocked: false, coinsCollected: coinsCollected);
}

Turnstile pushReducer(
    Turnstile state, Action<Null> action, ActionDispatcher dispatcher) {
  int visitorsPassed = state.visitorsPassed;
  if (!state.isLocked) {
    visitorsPassed++;
    print('Visitors passed: ${visitorsPassed}');
  }
  return state.copyWith(isLocked: true, visitorsPassed: visitorsPassed);
}

Now get it all together:

void main() {
  // Create our machine and register reducers:
  final builder = new StateMachineBuilder<Turnstile>(
    initialState: new Turnstile(true, 0, 0));
  builder
    ..bind(Actions.putCoin, putCoinReducer)
    ..bind(Actions.push, pushReducer);
  final machine = builder.build();

  // Try triggering some actions
  machine.dispatch(Actions.push());
  machine.dispatch(Actions.putCoin());
  // .. etc.
  // Make sure to dispose the machine in the end:
  machine.dispose();
}

Chaining actions #

Sometimes it is useful to trigger another action from inside current reducer. It is possible via ActionDispatcher argument passed to each reducer function. Simply invoke it dispatcher(yourNextAction(payload)); before returning updated state, e.g.:

State exampleReducer(
    State state, Action<Null> action, ActionDispatcher dispatcher) {
  // do work here
  // ...

  // State machine will call reducer for `otherAction` with the state object 
  // returned from this reducer.
  dispatcher(Actions.otherAction());

  return state.copyWith(exampleField: 'value');
}

Middleware example 1: logging #

ReduxMachine and Store classes expose events stream which contains all dispatched actions and their results. So logging middleware becomes a simple stream subscription. Printing to stdout:

final Store<MyState> store = getStore();
// Print all events, including errors and don't cancel subscription
// if error occurred.
store.events.listen(print, onError: print, cancelOnError: false);

Middleware example 2: error reporting #

Similarly to logging example we just need to report only errors:

final Store<MyState> store = getStore();
// No-op for normal store events, print error events and don't cancel
// subscription if error occurred.
store.events.listen(() {}, onError: print, cancelOnError: false);

Middleware example 3: making HTTP request #

final Store<MyState> store = getStore();
// Note that async is allowed in event listeners
store.eventsWhere(Actions.fetchUser).listen((event) async {
  try {
    int userId = event.newState.fetchingUserId;
    final user = await fetchUser(userId);
    store.dispatch(Actions.userFetched(user));
  } catch (error) {
    store.dispatch(Actions.userFetchFailed(error));
  }
});

// Assuming there is a reducer which simply sets
// store.state.fetchingUserId = action.payload; // 123 in this case
store.dispatch(Actions.fetchUser(123));

Obviously the above is not really a middleware, but it's main purpose is to move side-effects outside of the state store.

Features and bugs #

Please file feature requests and bugs at the issue tracker.

0
likes
0
pub points
0%
popularity

Publisher

unverified uploader

Redux State Machine and Store with no side-effects.

Repository (GitHub)
View/report issues

License

unknown (LICENSE)

More

Packages that depend on redux_machine