skip to content

In flutter_redux, when do StoreConnector's onInit, onWillChange, onDidChange and onInitialBuild run, and which one should trigger navigation?

level: middleimportance: nice to knowfreq 20%

answer

  1. onInit in initState, before the first view model
  2. onWillChange before the rebuild
  3. onDidChange after the frame
  4. onInitialBuild after the first frame
  5. navigate from onWillChange

basics

~20 s

onInit runs in initState before the first view model is built, onWillChange runs on a change before the rebuild, onDidChange and onInitialBuild run after the frame via post-frame callbacks, and onDispose runs in dispose. Navigation belongs in onWillChange.

solid answer

~40 s

flutter_redux's `StoreConnector` exposes lifecycle hooks around its internal `State`. `onInit(store)` runs in `initState` **before** the first `converter` call, so it is the place to dispatch a "load" action — the first build already sees its result if the reducer handles it synchronously. `onWillChange` runs for each accepted change before the builder is called; since 0.6.0 it receives the previous and the new view model, which suits imperative reactions such as pushing a receipt route when a payment turns successful. `onDidChange` runs after the frame through a post-frame callback, and `onInitialBuild` once after the first build with the initial view model, for things like a snackbar. `onDispose(store)` runs in `dispose`. With `distinct: true`, the change hooks fire only when the view model really changed. The library's own docs warn against navigating from `onDidChange`.

code

dart · 17 lines
dart
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';

class PosState {
  const PosState(this.offline);
  final bool offline;
}

Widget offlineBanner() => StoreConnector<PosState, bool>(
      distinct: true,
      converter: (store) => store.state.offline,
      onInitialBuild: (offline) {
        if (offline) debugPrint('Till started offline');
      },
      builder: (context, offline) =>
          offline ? const Text('Offline: card payments queued') : const SizedBox.shrink(),
    );

go deeper

for a junior

Remember onInit for loading on first display and onDispose for clean-up, both receiving the store.

for a middle

Explain the order: onInit before the first converter call, onWillChange before a rebuild, onDidChange and onInitialBuild after the frame, and how distinct gates them.

for a senior

Use onWillChange's previous and new view models to react to transitions such as a payment approval, and avoid navigation from post-frame hooks.

for a principal

Decide whether imperative reactions belong in widget callbacks at all, or in middleware and a navigation service, so flows stay testable without a widget tree.

## The hooks and where they run `StoreConnector` builds a private `StatefulWidget` that listens to the store. Its callbacks map onto that `State`'s lifecycle: | Callback | Receives | Runs | Typical use | |---|---|---|---| | `onInit` | the `Store` | in `initState`, before the first `converter` call | dispatch "load the open orders" | | `onInitialBuild` | the first view model | after the first build, via a post-frame callback | show a snackbar once | | `onWillChange` | previous and new view model | on each accepted change, before the builder runs | navigate, drive a `TabController` | | `onDidChange` | the new view model | after the rebuilt frame, via a post-frame callback | start an animation | | `onDispose` | the `Store` | in `dispose` | dispatch "clear stale data" | ## onInit: dispatch before the first view model Because `onInit` runs before the converter produces the first view model, an action it dispatches is already reflected in the first build when the reducer handles it synchronously — flutter_redux's tests check exactly that. In the point-of-sale app, the open-orders screen dispatches `LoadOpenOrders()` from `onInit`; middleware starts the fetch, and a reducer sets a loading flag the first frame can show. ## onWillChange: react before the rebuild For each change that passes `ignoreChange` and `distinct`, the connector calls `onWillChange(previous, next)` and then emits the new view model to its builder. - **0.6.0 made this a breaking change**: it now receives both the previous and the new view model, so you can react to transitions ("payment went from pending to approved") instead of to states. - flutter_redux's docs recommend it for imperative calls such as `Navigator` and `TabController`. ## onDidChange and onInitialBuild: after the frame Both are scheduled with `WidgetsBinding.instance.addPostFrameCallback`, so they run once the frame containing the new build has been laid out: - `onInitialBuild` runs once, with the view model from the first build. - `onDidChange` runs after each accepted change. The library documents that using a `BuildContext` inside `onDidChange` to navigate can cause problems and points to `onWillChange` for navigation. ## How distinct gates the change hooks With `distinct: false` (the default) every store change calls `onWillChange` and `onDidChange`, even when nothing this widget shows changed. Navigation reacting to a transition would then need its own comparison. With `distinct: true` and a value-equal view model, the hooks fire only for real changes — one more reason to give view models proper `==`. ## A payment flow, end to end 1. The checkout screen's connector converts the store into `CheckoutVm(status: ...)` with `distinct: true`. 2. The cashier taps "Charge"; middleware talks to the terminal and dispatches `PaymentApproved`. 3. The reducer sets `status` to approved, and the store emits. 4. `onWillChange(previous, next)` sees the transition from pending to approved and pushes the receipt route. 5. The builder redraws the checkout screen underneath. ```dart import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; enum PayStatus { idle, pending, approved } class CheckoutVm { const CheckoutVm(this.status); final PayStatus status; @override bool operator ==(Object other) => other is CheckoutVm && other.status == status; @override int get hashCode => status.hashCode; } class PosState { const PosState(this.status); final PayStatus status; } class LoadOpenOrders {} final navigatorKey = GlobalKey<NavigatorState>(); Widget checkout() => StoreConnector<PosState, CheckoutVm>( distinct: true, onInit: (store) => store.dispatch(LoadOpenOrders()), converter: (store) => CheckoutVm(store.state.status), onWillChange: (previous, next) { if (previous?.status != PayStatus.approved && next.status == PayStatus.approved) { navigatorKey.currentState?.pushNamed('/receipt'); } }, builder: (context, vm) => Text('Status: ${vm.status.name}'), ); ``` The `previous?.status` guard is written for the later, null-safe flutter_redux releases, where the previous view model is nullable; in the pinned 0.6.0 source it is simply the last view model.

  • Why is onInit a better place than build to dispatch a load action?
    `build` can run many times — on every parent rebuild and every store change — so dispatching there repeats the load and can loop, because the dispatch itself triggers another rebuild. `onInit` runs once in `initState`, before the first view model is built.
  • What changed about onWillChange in flutter_redux 0.6.0, and why does it matter?
    It became a breaking change: the callback now receives the previous view model and the new one instead of only the new one. That lets code react to a transition — for example, navigating only when a payment moves from pending to approved — without storing the last value itself.

saying these in an interview costs you the question

  • onInit runs after the first build, so the first frame never reflects its dispatch.
  • onDidChange is the recommended place to call Navigator.
  • onWillChange receives only the new view model in 0.6.0.
  • With distinct: false, onWillChange fires only when the view model changes.
  • Dispatching a load action inside the builder is equivalent to onInit.