esamattis / immer-reducer

Type-safe and terse reducers with Typescript for React Hooks and Redux
http://npm.im/immer-reducer
MIT License
224 stars 15 forks source link

Multiple reducers consuming one action #34

Open skeet70 opened 5 years ago

skeet70 commented 5 years ago

It's a semi-common pattern to have a single action created by an async action (like LOAD_USER or UPDATE_FOO) that multiple reducers listen to and pull state from. It seems like this isn't possible to me with immer-reducer. Is this possible, or is there a low boilerplate alternative?

esamattis commented 5 years ago

I've never had a use case for this. Can you provide an example?

This actually explicitly forbidden in immer-reducer now:

https://github.com/epeli/immer-reducer/blob/1c757d5ac57f51c2ecb7775d6498813156872256/src/immer-reducer.ts#L193-L197

Eg. if the assertion would be removed following would be possible:

class Foo1 extends ImmerReducer<State> {
  customName = "bar"
  setFoo(name: string) {
    this.draftState.foo1 = name;
  }
}

class Foo2 extends ImmerReducer<State> {
  customName = "bar"
  setFoo(name: string) {
    this.draftState.foo2 = name;
  }
}

By default immer-reducer generates the action types from the class name & method name but with the customName property it is possible to override the class name. If it is same between multiple classes their methods will receive the same action no matter which method is used to dispatch the action.

But there's reason why this is forbidden: The setFoos between the classes has no relation with each other. They can have completely different type signatures and it would be up to the user to make sure they are the same. Which is against the design goal of immer-reducer to be as type-safe as possible.

esamattis commented 5 years ago

As a workaround I would recommend just dispatching different actions from your async action:

dispatch(Actions.setLoadedUserForBar(user));
dispatch(Actions.setLoadedUserForFoo(user));

react-redux now days even exports the batch helper which avoids extra renders when used:

batch(() => {
  dispatch(Actions.setLoadedUserForBar(user));
  dispatch(Actions.setLoadedUserForFoo(user));
});

Also if the actions/reducers are in the same class you can alternatively define a combining action/reducer:

setLoaderUser(user: user) {
  this.setLoadedUserForBar(user);
  this.setLoadedUserForFoo(user);
}
skeet70 commented 5 years ago

An easy example (mentioned by redux starter kit) is a USER_LOGGED_OUT action that many reducers across the application may need to change their slice of state in response to.

That project seems to have similar goals and the way they handle it is by having an extraReducers option when creating a slice (which is similar in structure to your Reducer class). You use extraReducers to provide references to imported actions from other places that this reducer should listen to. This introduces its own edge cases to watch out for that they call out, but it does solve the general issue in a pretty type safe way.

esamattis commented 5 years ago

I see.

I'd really like to solve this with decorators too:

@action("LOAD_USER")
setUserLoggedIn(user: User) {
 // ...
}

Where the @action() decorator would force the action type to be what ever you want and you could share action types using it. This does not solve the typing issue (I don't think its solvable to beging with).

zdila commented 4 years ago

I would also appreciate to have this feature. I use it often in the projects I work on. One is for example https://github.com/FreemapSlovakia/freemap-v3-react and such an action there is for example clearMap. I am trying to rewrite it to use immer-reducer as I started to have a need to access state "from a different branch" in a reducer and immer-reducer composeReducers makes it easy.