Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Options for persistence relating to where we put our business logic and using middleware. #64

Closed
atreeon opened this issue Jul 17, 2018 · 11 comments

Comments

@atreeon
Copy link

atreeon commented Jul 17, 2018

Hi Brian, many thanks for the library once again 馃憤

What options do we have / what recomendations can you give for persistance? The main problem I'm having is that I put a lot of my business logic in my reducers. This seemed to be working out until I tried to add some persistence middleware

  1. dispatch(new CalculatePersonsAgeAction);
  2. middleware - persist data
  3. reducer - run calculate age logic and update record in the store

(as you can probably work out the persist will be calculated too early and will save old information, also some of my actions just pass an id such as the following action so I need to lookup the record...

ToggleExcludedAction(int myId)

this is handled in my reducer where I look up the object and do a copyWith to alter my record.

So this was my first problem, I am running my logic before I need to persist it, so...my next plan was to put more of my logic in my actions and I started to use redux thunk where it was necessary to get at the state (ie lookup an existing record by id). My next problem is that now my actions are tied to AppState and my tests aren't as clean becase I need to create AppState (previously I could create a simple Store based on just the data relevant to the reducer).

...here is the new Action...

Function(Store<AppState>) toggleExcludedAction(int myId) {
  return (Store<AppState> store) {

which I feel makes it a little less clear in the test because now I have to create the AppState in my tests where a lot of the data is irrelevant to the test (previously I was just creating just the data that the reducer was based on which made it clear what the data dependencies were). It's not the end of the world but it's just not as clean and wondered if I had missed something or if there is an alternative option.

I'm now considering just putting the code into a function that accepts the required dependencies and letting the action (which has a dependency on AppState) call that function and only testing my function.

so I test this

void userFlagSeen(Function(dynamic) dispatch, List<UserFlag> userFlags, int id) {
  var existingUserFlag = userFlags.firstWhere((uf) => uf.id == id);
  var newUserFlag = existingUserFlag.copyWith(hasSeen: true);
  dispatch(new UserFlagUpdateA(newUserFlag));
}

but not this

Function(Store<AppState>) userFlagSeenAction(int lectureId) {
  return (Store<AppState> store) =>
      _userFlagSeenAction(store.dispatch, store.state.userFlags, lectureId);
}

ok...now I'm overthinking this problem but here is a more type safe class based approach

class Action<T>{
  Function(Store<AppState>) byId(Actions<T> actions, int id) {
    return (Store<AppState> store) =>
        actions.updateAction(store.dispatch, new T().storeStateFn, id);
  }
}

abstract class Actions<T> {
  void updateAction(Function dispatch, List<T> objects, int id);
}

class HasSeen implements Actions<UserFlag> {
  @override
  void updateAction(Function dispatch, List<UserFlag> userFlags, int id) {
    var newUserLecture = new UserFlag(id, true);
    dispatch(new UserFlagUpdateAction(newUserLecture));
  }
}

calling it like so

dispatch(new Action<UserFlag>().byId(new HasSeen(), 1))

there may be an easier way to this though!

@brianegan brianegan added this to Todo in Things to do Jul 18, 2018
@brianegan brianegan moved this from Todo to DuDone in Things to do Jul 18, 2018
@brianegan brianegan moved this from DuDone to Todo in Things to do Jul 18, 2018
@brianegan
Copy link
Owner

brianegan commented Jul 18, 2018

Hey, happy to help out. I think it all comes down to this question:

(as you can probably work out the persist will be calculated too early and will save old information, also some of my actions just pass an id such as the following action so I need to lookup the record...

In that case, just use next(action) before you perform your work in the Middleware! This will forward the action on to the reducer synchronously, then when you access store.state in the middleware after the next(action) call, you'll have the updated info!

Example:

logPreviousStateMiddleware(Store<int> store, action, NextDispatcher next) {
  // Print the *previous* state
  print('${new DateTime.now()}: ${store.state}');

  // Then forward the action to your reducer
  next(action);
}

vs

logNextStateMiddleware(Store<int> store, action, NextDispatcher next) {
  // Forward the action to your reducer synchronously
  next(action);

  // Print the *new* state
  print('${new DateTime.now()}: ${store.state}');
}

Does this work for ya? Might be a simpler approach than some of the solutions proposed above.

@atreeon
Copy link
Author

atreeon commented Jul 19, 2018

This seems like it should help yes...I understand the theory.

I have another problem which I think, in a round about way, affects my first question and the general pickle I seem to have stumbled into.

I have a few bits of middleware which then dispatch other actions, and those actions are sent to the reducers. The order in which they hit the reducers seems to be out of sync to when I called initially 'actioned' the middleware. Is there any way to force these dispatches to middleware to 'complete' before the next, synchronously?

@atreeon
Copy link
Author

atreeon commented Jul 19, 2018

(I'm guessing the order is just based on where they are defined in the store's middleware).

Should I use a thunk instead, there are two reasons for using middleware or a thunk

  1. to dispatch other actions
  2. to get access to data in the store (ie my PeopleReducer does not have access to state.employees)

@atreeon
Copy link
Author

atreeon commented Jul 19, 2018

I guess following on from this, is there a way to ensure that the thunks are dispatched and caught by the reducer in order?

Also, another problem, I do
dispatch(thunk1) -> in here, dispatch(new UpdatePersonAction())
dispatch(thunk2) -> in here I access state.people

in my thunk2 I think the state.people value is old and hasn't taken into account the work done in thunk1 (ie the dispatch(thunk2) executes before dispatch(newUpdatePersonAction()) in thunk1

@atreeon
Copy link
Author

atreeon commented Jul 19, 2018

Would it be ok to pass the additional state required into the reducer?

int counterReducer(int count, int multiplier, dynamic action) {
  if (action == Actions.IncrementCounter) {
    return (count + 1) + multiplier;
  }

  return count;
}

https://github.com/twistedinferno/flutter_redux_counter

@atreeon
Copy link
Author

atreeon commented Jul 20, 2018

again...I'm probably overcomplicating things (and sharing too many of my thoughts!!!) but...I created a new combineReducers2 which allows a typed reducer to take further data

final counterReducers = combineReducers2<int, int>([
  new TypedReducer2<int, int, IncrementCounterMultipliedAction>(_incrementCounterMultipliedAction)
], [
  new TypedReducer<int, IncrementCounterAction>(_counterAction)
]);

https://github.com/twistedinferno/flutter_redux_counter/blob/combinereducers2/lib/main.dart

@brianegan
Copy link
Owner

Yo yo -- sorry for the delay, busy times!

(I'm guessing the order is just based on where they are defined in the store's middleware).

Yep!

If you need to coordinate when actions are dispatched from a middleware to the Store, it might make more sense to coordinate all of that logic within a single Middleware (since it seems these things are related).

I guess following on from this, is there a way to ensure that the thunks are dispatched and caught by the reducer in order?

Kinda the same answer as above. If order is important, I'd coordinate that stuff in a central place where you have full control.

Would it be ok to pass the additional state required into the reducer?

Fa sho! As long as your creating a Reducer in the end, how you compose that function is entirely open ended. I might put the multiplier in an Action myself and use a normal Reducer, but if TypedReducer2 works for ya, go for it.

@brianegan
Copy link
Owner

Hey @twistedinferno -- did this help? Happy to close the issue if you're feeling good, or answer more Qs if ya got more :)

@atreeon
Copy link
Author

atreeon commented Aug 3, 2018

Hi Brian, thanks again for the responses :-)

I think I've got it, yup happy for you to close this.

Co-ordinating things in the middleware seems to have solved my problems along with the knowledge that my reducer will be in a post appstate after next(action) has been called (I had some problems here mostly due to making calls in the wrong order and misunderstanding how thunks will be processed). I'm not directly in this space right now (back in the front end) but my examples I have been tinkering with seem to be getting me where I want to be...I think the combineReducers2 thing is overcomplicating my scenario but it's good to get further into a library and understand it more (maybe it might be fine too!)

(btw, thanks for a great talk in London a few weeks ago! I had to rush off so didn't have a chance to say hi but hopefully another time)

@brianegan
Copy link
Owner

Cool, glad it worked out :) Would have been fun to meet, but I'm sure I'll be in London again in the coming months! If there's another Flutter meetup I'll definitely join.

@atreeon
Copy link
Author

atreeon commented Aug 3, 2018

Yup, I'll definitely make sure I have time to say hello this time :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Development

No branches or pull requests

2 participants