reactLatest<Value> method

System<State, Event> reactLatest<Value>({
  1. required Value value(
    1. State state
    ),
  2. Equals<Value>? equals,
  3. bool skipInitialValue = true,
  4. required Disposer? effect(
    1. Value value,
    2. Dispatch<Event> dispatch
    ),
})

Add effect triggered by react state's partial value change, it will cancel previous effect when new effect triggered or system disposal.

API Overview

searchSystem
  ...
  .reactLatest<String>(
    value: (state) => state.keyword, // map state to value, required
    equals: (value1, value2) {  // `equals` is used to determine if old value equals 
      return value1 == value2;  // to new value. If there are not equal, then effect
    },                          // is triggered. `equals` is nullable, defaults to 
                                // `==` as shown.
    skipInitialValue: true, // return true if initial value is skipped,
                            // which won't trigger effect.
                            // return false if initial value isn't skipped,
                            // which will trigger effect.
                            // `skipInitialValue` defaults to true if omitted.
    effect: (value, dispatch) { 
      // trigger effect here with new value, required
      return Disposer(() { // return a `Disposer` to register cancel logic 
                           // with this ticket.
                           // return null or omit return, if there is nothing
                           // to cancel.
      });
    },
  )
  ...

Usage Example

Below code showed how to model search bar, latest search words cancel previous search API call if previous one is not completed.

searchSystem
  ...
  .reactLatest<String>(
    value: (state) => state.keyword,
    effect: (keyword, dispatch) async {
      try {
        final data = await api.call(keyword);
        dispatch(LoadDataSuccess(data));
      } on Exception {
        dispatch(LoadDataError());
      }
    },
  )

For this scenario if previous search result came after latest one, the previous result will be ignored.

If search api provide a cancellation mechanism, We can return a Disposer to register cancel logic with this ticket.

For example if above api.call return Stream:

searchSystem
  ...
  .reactLatest<String>(
    value: (state) => state.keyword,
    effect: (keyword, dispatch) async {
      final stream = api.call(keyword); // it return stream now
      final subscription = stream.listen(
        (data) => dispatch(LoadDataSuccess(data)),
        onError: (Object _) => dispatch(LoadDataError()),
      );
      return Disposer(() => subscription.cancel()); // register cancel
    },
  )
  ... 

This Disposer will be called when keyword changed or system disposal.

Implementation

System<State, Event> reactLatest<Value>({
  required Value Function(State state) value,
  Equals<Value>? equals,
  bool skipInitialValue = true,
  required Disposer? Function(Value value, Dispatch<Event> dispatch) effect,
}) {
  final localEquals = equals ?? defaultEquals;
  return withContext<_ReactLatestContext<Value, Event>>(
    createContext: () => _ReactLatestContext(),
    effect: (context, state, oldState, event, dispatch) {
      final reactContext = context.reactContext;
      final localValue = value(state);
      final bool shouldUpdateOldValue;
      final bool shouldTriggerEffect;
      if (event == null) {
        shouldTriggerEffect = !skipInitialValue;
        shouldUpdateOldValue = true;
      } else {
        final oldValue = reactContext.oldValue as Value;
        shouldTriggerEffect = !localEquals(oldValue, localValue);
        shouldUpdateOldValue = shouldTriggerEffect;
      }
      if (shouldUpdateOldValue) {
        reactContext.oldValue = localValue;
      }
      if (shouldTriggerEffect) {
        final latestContext = context.latestContext;
        latestContext.disposePreviousEffect();
        latestContext.disposer = effect(localValue, latestContext.versioned(dispatch));
      }
    },
    dispose: (context) {
      context.latestContext.disposePreviousEffect();
    },
  );
}