pianostringquartet / reframe-middleware

Clojurescript re-frame-style middleware for Dart and Flutter

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

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

pub package

Inspired by Clojurescript's re-frame.

Flutter demo.

How to use

1. Add reframe_middleware to your pubspec.yaml:

dependencies:
  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';

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

Asynchronous, impure action (side-effect):

import 'package:reframe_middleware';

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

An action that does both:

@immutable
class DoubleIncrementAction extends ReframeAction {
  @override
  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.

store.dispatch(IncrementAction());

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.

@immutable
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 => [];

For state updates, reframeMiddleware dispatches a special action StateUpdate to carry the new state to the reframeReducer.

For side-effects, reframeMiddleware runs the Future and dispatches the resulting actions.

// middleware
Middleware<S> reframeMiddleware<S, E>(E effects) =>
    (Store<S> store, dynamic event, NextDispatcher next) {
      if (event is ReframeAction) {
        event.handle(store.state, effects)
            // sends new state to reducer via StateUpdate action
          ..nextState
              .ifPresent((newState) => store.dispatch(StateUpdate(newState)))
           // runs side effects and dispatch resulting actions:
          ..effect().then((events) => events.forEach(store.dispatch));
      }

      // passes (1) the event to next middleware (e.g. 3rd party middleware)
      // and (2) a StateUpdate to the reducer
      next(event);
    };


// reducer
S reframeReducer<S>(S state, dynamic event) =>
    event is StateUpdate ? event.state : state;

FAQ

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

About

Clojurescript re-frame-style middleware for Dart and Flutter

License:BSD 3-Clause "New" or "Revised" License


Languages

Language:Dart 100.0%