redux_machine 0.1.2

  • README.md
  • CHANGELOG.md
  • Example
  • Installing
  • Versions
  • 77

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.

Changelog

0.1.2

  • Added onError argument to StoreBuilder and StateMachineBuilder.
  • Fixed: don't swallow errors in action dispatch flow.
  • Removed StoreError class.

0.1.1

  • Added type argument to StoreEvent for the action payload type for better static analysis.
  • Added store field to StoreEvent which contains reference to the state Store (or StateMachine) which produced that event.
  • Added Store.changesFor to allow listening for changes on a part of the application state.

0.1.0

  • Deprecated ReduxMachine implementation in favor of new StateMachine class.
  • Added separate Redux Store implementation which can be used on its own. New StateMachine uses Store internally for state management.
  • Updated readme with some details on side-effects handling in this library.

0.0.1

  • Initial version, created by Stagehand

example/main.dart

// Copyright (c) 2017, Anatoly Pulyaevskiy. All rights reserved. Use of this source code
// is governed by a BSD-style license that can be found in the LICENSE file.

import 'package:redux_machine/redux_machine.dart';

/// Implementation of a simple coin-operated turnstile state machine
/// as described here:
/// https://en.wikipedia.org/wiki/Finite-state_machine#Example:_coin-operated_turnstile
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());
  machine.dispatch(Actions.push());
  machine.dispatch(Actions.push());
  machine.dispatch(Actions.push());
  machine.dispatch(Actions.push());
  machine.dispatch(Actions.putCoin());
  machine.dispatch(Actions.putCoin());
  machine.dispatch(Actions.push());
  machine.dispatch(Actions.push());
  // .. etc.
  // Make sure to dispose the machine in the end:
  machine.dispose();
}

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

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

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

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  redux_machine: "^0.1.2"

2. Install it

You can install packages from the command line:

with pub:


$ pub get

with Flutter:


$ flutter packages get

Alternatively, your editor might support pub get or flutter packages get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


      import 'package:redux_machine/redux_machine.dart';
  
Version Uploaded Documentation Archive
0.1.2 Feb 1, 2018 Go to the documentation of redux_machine 0.1.2 Download redux_machine 0.1.2 archive
0.1.1 Jan 31, 2018 Go to the documentation of redux_machine 0.1.1 Download redux_machine 0.1.1 archive
0.1.0 Jan 31, 2018 Go to the documentation of redux_machine 0.1.0 Download redux_machine 0.1.0 archive
0.0.1 Jan 11, 2018 Go to the documentation of redux_machine 0.0.1 Download redux_machine 0.0.1 archive
1.0.0-rc.2 Jun 1, 2018 Go to the documentation of redux_machine 1.0.0-rc.2 Download redux_machine 1.0.0-rc.2 archive
1.0.0-rc.1 Apr 2, 2018 Go to the documentation of redux_machine 1.0.0-rc.1 Download redux_machine 1.0.0-rc.1 archive
1.0.0-dev.1.0 Mar 23, 2018 Go to the documentation of redux_machine 1.0.0-dev.1.0 Download redux_machine 1.0.0-dev.1.0 archive
1.0.0-beta.1 Apr 2, 2018 Go to the documentation of redux_machine 1.0.0-beta.1 Download redux_machine 1.0.0-beta.1 archive

Analysis

We analyzed this package on Jun 19, 2018, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.0.0-dev.63.0
  • pana: 0.11.3

Scores

Popularity:
Describes how popular the package is relative to other packages. [more]
55 / 100
Health:
Code health derived from static analysis. [more]
99 / 100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100 / 100
Overall score:
Weighted score of the above. [more]
77
Learn more about scoring.

Platforms

Detected platforms: Flutter, web, other

No platform restriction found in primary library package:redux_machine/redux_machine.dart.

Suggestions

  • The description is too short.

    Add more detail about the package, what it does and what is its target use case. Try to write at least 60 characters.

  • Package is pre-v1 release.

    While there is nothing inherently wrong with versions of 0.*.*, it usually means that the author is still experimenting with the general direction API.

  • Fix analysis and formatting issues.

    Analysis or formatting checks reported 4 hints.

    Strong-mode analysis of lib/redux_machine.dart gave the following hint:

    line: 34 col: 32
    'ReduxMachine' is deprecated and shouldn't be used.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=1.24.2 <2.0.0
Dev dependencies
test ^0.12.0