marcglasberg / async_redux

Flutter Package: A Redux version tailored for Flutter, which is easy to learn, to use, to test, and has no boilerplate. Allows for both sync and async reducers.
Other
232 stars 41 forks source link

Recommendation on how to dispatch events based on state changes #29

Closed cgestes closed 5 years ago

cgestes commented 5 years ago

Hi,

I would like to react to my authentication state and be able to emit events when it changes.

For now I do that outside of the store, I wonder if there is a simple way to subscribe to state changes and react to them.

I envision something that looks like this:

void setupAuthStateChanges(store) {
  // we have a selector for our changes
  var selector = (state) => state.auth.profile; //assuming this is immutable

  // we have an action in response
  var action = (store, profile) {
    if (profile == null)
      store.dispatch(DeInitSomethingElse());
    else
      store.dispatch(InitSomethingElse());
  }

  // we register the action and the selector
  subscription = store.onChange(selector, action);

  // later on we can cancel the subscription
  subscription.cancel();
}

Seems pretty similar to StoreConnector but without the widget logic.

I wonder how you do it guys.

marcglasberg commented 5 years ago

I'm not sure I understand what you want. You want to observe certain parts of the state, and then dispatch some actions when they change? You can do that using the StateObserver (search for it in the documentation), but instead you should probably do that inside of the action that changed state.auth.profile to start with.

Can you show me the code of the action that changed state.auth.profile?

cgestes commented 5 years ago

The code that changes the state of state.auth.profile:

class SetProfileEffect extends ReduxAction<AppState> {
  final UserProfile profile;

  SetProfileEffect(this.profile);

  @override
  AppState reduce() {
    return state.copy(auth: state.auth.copy(profile: profile));
  }
}

This action is dispatched by another authentication action that uses firebase_auth.

I have AuthState for auth management, and ContentState for the content of the app. ContentState should start running once the user is authenticated.

class AppState {
  final AuthState auth;
  final ContentState content;
}

I consider AuthState to be independant from ContentState.

StateObserver seems to do only part of the job, it doesn't filter state changes.

I started experimenting with the following solution:

import 'package:async_redux/async_redux.dart';
import 'package:collection/collection.dart';

class StoreReactor<SubState, State> {
  var oldValue;
  var subscription;

  final Store<State> store;
  final SubState Function(State) selector;
  final void Function(Store<State>, SubState state) action;

  StoreReactor(this.store, this.selector, this.action) {
    assert(subscription == null);
    oldValue = selector(store.state);
    subscription = store.onChange.listen(_onChange);
  }

  _onChange(State state) {
    var newValue = selector(state);

    bool hasChanges =
        DeepCollectionEquality.unordered().equals(oldValue, newValue) == false;
    if (hasChanges == false) return;
    oldValue = newValue;
    action(store, newValue);
  }

  unsubscribe() {
    subscription.cancel();
  }
}

used like this:

var authReactor;

void setupAuthStateChanges(store) {
  assert(authReactor == null);

  var selector = (state) => state.auth.profile;

  // we have an action in response
  var action = (store, profile) {
    if (profile != null) {
      store.dispatch(InitializeContentAction());
    } else {
      store.dispatch(ResetContentAction());
    }
  };

  authReactor = StoreReactor(store, selector, action);
}
marcglasberg commented 5 years ago

In my opinion you are overcomplicating this. You could just do:

class SetProfileEffect extends ReduxAction<AppState> {
  final UserProfile profile;

  SetProfileEffect(this.profile);

  @override
  AppState reduce() {

    if (profile != null) {
      dispatch(InitializeContentAction());
    } else {
      dispatch(ResetContentAction());
    }

    return state.copy(auth: state.auth.copy(profile: profile));
  }
}
cgestes commented 5 years ago

That's right, I guess it's a matter of style. Anyway this is solved. Thank you. I can now react to state changes outside the widget hierarchy.

cgestes commented 5 years ago

For reference, what I wanted to achieve: https://felangel.github.io/bloc/#/architecture?id=bloc-to-bloc-communication

I had to switch to bloc, cause the StoreReactor was racy, returning Future in reduce actions was allowing other dispatch to run between the end of the reduce function and the effective application of the state on the store, leading to part of the state being overwritten.

marcglasberg commented 5 years ago

Well, Redux has its own way of doing things, and I guess that observing state changes to dispatch actions, while totally possible, is not how it's supposed to be done, in principle. I guess your StoreReactor was trying to use Redux to do Bloc, which is bound to create problems, or at least to make it difficult for you to obtain help from other people who are more closely following Redux principles, and may not be able to give you advice when you depart from those principles (not that what you were trying to do was wrong at all, but just different). As I said, the usual approach would be to just dispatch InitializeContentAction/ResetContentAction from inside the reducer that changed your profile state. This would be simple and it works.

Also, Redux is all about being easy to reason about what's going on. Observing state to dispatch actions may reduce coupling, but it also makes it much more difficult to reason about what is going on. You are changing some state, and then unless you know all of your code you have no idea if some other actions are being dispatched because of that state change. On the contrary, if you just dispatch InitializeContentAction/ResetContentAction from inside the reducer that changed profile, then it's easy to see exactly what's going on.

cgestes commented 5 years ago

That's true, I guess I always hope for my substore/subaction to be modular when I use redux, while in fact it should be thought as a global store for the application. I have always been puzzled with that when using Redux, but now that I discovered Bloc, it makes much more sense to me, and that's also how I would like to do React now :)

I really like the way you merged the reducer in the action :)

Btw I commented on another issue about the race if you are interested :)