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

Dart 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 to stdout:
store.events.listen(print);

Middleware example 2: error reporting #

Since action dispatch flow is side-effect free handling exceptions in reducers is straightforward. To track unhandled errors you can set the onError handler on StoreBuilder (and StateMachineBuilder):

final StoreBuilder<MyState> builder = new StoreBuilder(
  onError: errorHandler);

// Example error handler
void errorHandler(MyState state, Action action, error) {
  // Avoid having async logic in here.
  errorsSync.add(error);
  // Throw the error in the end.
  throw error;
}

The onError handler is executed as part of the action dispatch flow therefore it must be pure. Instead of doing any async logic inside the handler consider leveraging an EventSink to collect errors and publish asynchronously.

If onError is omitted it defaults to a handler which simply throws all errors.

Actions which resulted in an error are not published to the events stream.

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));

Features and bugs #

Please file feature requests and bugs at the issue tracker.

0
likes
30
pub points
0%
popularity

Publisher

unverified uploader

Redux State Machine and Store with no side-effects.

Repository (GitHub)
View/report issues

License

BSD-3-Clause (LICENSE)

More

Packages that depend on redux_machine