Inspired by Clojurescript's re-frame.
dependencies:
reframe_middleware: ^1.0.0
import 'package:reframe_middleware';
final store = Store<AppState>(
reframeReducer, // produces new state
initialState: AppState(),
middleware: [reframeMiddleware(), // handles actions
thirdPartyMiddleware, ...]);
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()]));
}
store.dispatch(IncrementAction());
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;
No. Reframe-middleware already does async logic -- that’s what ReframeResponse
's effect
is for.
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.
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:
- UI is explained by state ("state causes UI")
- 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.)