reframe-middleware: the ‘action first’ approach to redux

Reframe-middleware makes actions first class in redux.dart.

Inspired by Clojurescript's re-frame.

Flutter demo.

How to use

1. Add reframe_middleware to your pubspec.yaml:

  reframe_middleware: ^1.0.0

2. Add reframeReducer and reframeMiddleware to your redux.dart Store:

import 'package:reframe_middleware';

final store = Store<AppState>(
	reframeReducer, // produces new state
    initialState: AppState(), 
    middleware: [reframeMiddleware(), // handles actions
	             thirdPartyMiddleware, ...]);

3. Define an action:

Synchronous, pure action:

import 'package:reframe_middleware';

class IncrementAction extends ReframeAction {
  ReframeResponse<AppState> handle(AppState state) =>
        state.copy(count: state.count + 1));

Asynchronous, impure action (side-effect):

import 'package:reframe_middleware';

class AsyncIncrementAction extends ReframeAction {
  ReframeResponse<AppState> handle(AppState state) =>
      ReframeResponse.sideEffect(() =>
          Future.delayed(Duration(milliseconds: 1000))
              .then((_) => [IncrementEvent()]));

An action that does both:

class DoubleIncrementAction extends ReframeAction {
  ReframeResponse<AppState> handle(AppState state, Effects effects) {
    return ReframeResponse(
        nextState: Optional.of(state.copy(count: state.count + 1)),
        effect: () => Future.delayed(Duration(milliseconds: 1000))
            .then((_) => [IncrementAction()]));

4. Dispatch... and done.


How it works

Actions are handled by their own handle method:

action.handle(store.state) -> ReframeResponse

A ReframeResponse contains a new state and side effect.

class ReframeResponse<S> {
  final Optional<S> nextState;
  final SideEffect effect;

  const ReframeResponse({
    this.nextState = const Optional.absent(),
    this.effect = noEffect,
// A side-effect is a closure that becomes a list of actions
typedef SideEffect = Future<List<Action>> Function();
Future<List<Action>> noEffect() async => [];


Do I need thunkMiddleware?

No. Reframe-middleware already does async logic -- that’s what ReframeResponse's effect is for.

Does this replace redux.dart or flutter-redux?

No. Reframe-middleware is supposed to be used with redux.dart (in the same way e.g. Flutter redux_persist is).

Reframe-middleware, like redux.dart, can be used with or without the excellent flutter-redux.

Doesn't this couple reducers and actions, which is discouraged?

Short answer: Yes.

Long answer:

There have been objections to 1:1 mappings between actions and reducers. (“The whole point of Flux/Redux is to decouple actions and reducers”).

But the decoupling of actions and reducers is an implementation detail of redux.js.

In contrast, Clojurescript re-frame intentionally couples an event (action) with its handler (reducer + middleware). Why?

Every redux system* -- Elm, re-frame, redux.js, redux-dart etc. -- is characterized by two fundamental principles:

  1. UI is explained by state ("state causes UI")
  2. state is explained by actions ("actions cause state")

When we dispatch an action we ask, "What does this action mean, what updates or side-effects will it cause?"

If you need to reuse state modification logic, reuse a function -- don't reuse a reducer.

*(In contrast, SwiftUI has 1 but not 2, and so is not a redux sytem.)