In a Flutter redux app, how do you run async work such as a card-payment call through middleware, and what does a thunk middleware change?
answer
- reducers stay synchronous
- MiddlewareClass call(store, action, next)
- always forward with next(action)
- dispatch result actions later
- thunk: functions as actions
basics
~20 sMiddleware sees each action before the reducer: on a charge action it calls next(action), starts the payment future and later dispatches a success or failure action. A thunk middleware lets you dispatch a function that receives the store.
solid answer
~40 sReducers must stay synchronous and side-effect free, so async work goes in middleware. With the `redux` package, a middleware can be a class implementing `MiddlewareClass<PosState>` whose `call(Store<PosState> store, dynamic action, NextDispatcher next)` runs for every dispatch. For a `ChargeRequested` action it calls `next(action)` so the reducer can mark the payment pending, then starts the terminal call and later dispatches `PaymentApproved` or `PaymentFailed`. Forgetting `next(action)` silently swallows the action. A **thunk** middleware generalises this: if the dispatched value is a function, it calls that function with the store instead of forwarding it, so each async flow lives in its own function rather than in one growing middleware. Either way, cancellation and ordering are your job — flutter_redux's own search example cancels the previous operation with a timer and a `CancelableOperation`.
code
dart · 39 linesimport 'package:redux/redux.dart';
typedef PosThunk = Future<void> Function(Store<PosState> store);
// A minimal thunk middleware: functions run with the store, data goes on.
class ThunkMiddleware implements MiddlewareClass<PosState> {
@override
void call(Store<PosState> store, dynamic action, NextDispatcher next) {
if (action is PosThunk) {
action(store);
} else {
next(action);
}
}
}
PosThunk chargeCard(int amountCents, Future<String> Function(int) terminal) =>
(store) async {
store.dispatch(ChargeRequested(amountCents));
try {
store.dispatch(PaymentApproved(await terminal(amountCents)));
} catch (e) {
store.dispatch(PaymentFailed(e));
}
};
class PosState {}
class ChargeRequested {
ChargeRequested(this.amountCents);
final int amountCents;
}
class PaymentApproved {
PaymentApproved(this.receipt);
final String receipt;
}
class PaymentFailed {
PaymentFailed(this.error);
final Object error;
}go deeper
Know that reducers must stay synchronous and that API calls go into middleware, which dispatches new actions with the results.
Explain MiddlewareClass's call(store, action, next), why next(action) must be called, and when to forward before starting the async work.
Design the payment flow with duplicate-tap guards, error actions and tests, and compare a custom middleware, a thunk middleware and stream-based epics by where the code ends up.
Set the side-effect convention for the whole store — one mechanism, how it is tested, and how failures surface — so a legacy codebase stops growing three ways of doing async work.
## Why async work cannot live in reducers A reducer returns the next state synchronously from the current state and an action. A payment call returns a `Future`, and waiting for it inside a reducer is impossible without breaking that contract. Redux's answer is **middleware**: code that runs on every dispatch before the reducer and may perform side effects, including dispatching more actions later. ## Writing middleware with the redux package The `redux` package accepts a list of middleware in the `Store` constructor. A readable form is a class implementing **`MiddlewareClass<S>`**, whose `call` method receives: - the **`Store<S>`**, to read `state` and to `dispatch` follow-up actions; - the **action** (typed `dynamic`); - **`next`**, a `NextDispatcher` that passes the action on to the next middleware and finally to the reducer. A payment middleware for the point-of-sale app: 1. On `ChargeRequested`, call `next(action)` first, so the reducer can set the status to pending and the UI can show a spinner. 2. Start `terminal.charge(amountCents)`. 3. When it completes, `store.dispatch(PaymentApproved(receipt))`; on error, `store.dispatch(PaymentFailed(error))`. 4. For every other action, just call `next(action)`. The most common bug is **not calling `next(action)`**: the action never reaches the reducer or later middleware, and the screen silently does nothing. ## Thunk middleware: dispatching functions One middleware per feature grows into a large `if`/`else` over action types. A **thunk middleware** changes what can be dispatched: when the action is a function, the middleware calls it with the store and does not forward it; anything else goes to `next`. Each async flow becomes a function: - `store.dispatch(chargeCard(amountCents))`, where `chargeCard` returns a function of the store; - the function dispatches `ChargeRequested`, awaits the terminal, and dispatches the outcome. The pattern is small enough to write yourself with `MiddlewareClass`, which also shows exactly what it does. | Approach | Where the async code lives | Strength | Weakness | |---|---|---|---| | Custom middleware | a class that switches on action types | all side effects of a feature in one place | grows with every action | | Thunk middleware | a function dispatched as the action | each flow is local and easy to read | actions are no longer plain data, so they are harder to log or replay | | Stream-based epics | a function from an action stream to an action stream | debounce and cancel with stream operators | another package and another concept | ## Ordering and cancellation are yours Middleware does not cancel anything by itself. flutter_redux's search example shows the manual version: its `SearchMiddleware` keeps a `Timer` to debounce search actions and a `CancelableOperation` from the `async` package to drop the previous request's result, and it calls `next(action)` at the end. For payments, the relevant guard is usually the opposite — refuse a second `ChargeRequested` while one is pending, by checking `store.state` in the middleware. ## Testing Because middleware is plain Dart, it can be tested without widgets: create a `Store` with a fake terminal, dispatch `ChargeRequested`, await the fake, and assert on `store.state`. ```dart import 'package:redux/redux.dart'; class PaymentMiddleware implements MiddlewareClass<PosState> { PaymentMiddleware(this.charge); final Future<String> Function(int amountCents) charge; @override void call(Store<PosState> store, dynamic action, NextDispatcher next) { if (action is ChargeRequested && store.state.pending) { return; // ignore a second tap while a charge is running } next(action); if (action is ChargeRequested) { charge(action.amountCents).then( (receipt) => store.dispatch(PaymentApproved(receipt)), onError: (Object e) => store.dispatch(PaymentFailed(e)), ); } } } class PosState { const PosState({this.pending = false}); final bool pending; } class ChargeRequested { ChargeRequested(this.amountCents); final int amountCents; } class PaymentApproved { PaymentApproved(this.receipt); final String receipt; } class PaymentFailed { PaymentFailed(this.error); final Object error; } ```
- What happens if a middleware handles ChargeRequested but forgets to call next(action)?The action stops at that middleware: later middleware and the reducer never see it, so the pending flag is never set and the UI does not change. Only the follow-up actions the middleware dispatches itself reach the reducer. Always forward with `next(action)` unless swallowing the action is deliberate.
- Why call next(action) before starting the payment call rather than after it completes?Forwarding first lets the reducer record the request immediately — a pending status, a disabled Charge button — while the terminal works. Waiting for the future before forwarding would leave the UI unaware that anything is happening and allow duplicate taps.
saying these in an interview costs you the question
- Make the reducer async and await the payment call inside it.
- Middleware forwards actions automatically, so next(action) is optional.
- A thunk middleware forwards function actions to the reducers as well.
- Middleware cancels a stale request automatically when a newer action arrives.
- Dispatching functions keeps actions plain, loggable data.