ngrx/platform

Pass the root state as an optional argument to the reducers.

Closed this issue ยท 11 comments

Describe any alternatives/workarounds you're currently using

Is it possible for a feature reducer to require data from outside its slice of state.
The most common scenario that I have encountered is to access 'global properties' like configurations stored on the application state.

This can be achieved by passing the root state as the last parameter for the reducer function invocation. It should not break existing functionalities.

The reducer does receive all the actions so it could react to it but then it cannot 'read' another feature's state slice.
The reducer should not modify this 'root state' and it should continue returning its own feature's state slice. It can use this new parameter for read-only operations.

If accepted, I would be willing to submit a PR for this feature

[ ] Yes (Assistance is provided if you need help submitting a pull request)
[X] No

I think this is something we don't want to add, because

  • this is against the redux principle ((state, action) => state)
  • it allows you to modify the global state from every reducer, making it harder to wrap your head around

The Redux docs - FAQ Reducers offers some insight on how you could tackle this.

If you need some help or want to verify something you can ask it in the gitter channel, or on stackoverflow (with a ngrx tag).

@timdeschryver I'm sorry but I have to disagree with you. The (most popular, IMO) solution using redux is to create your own version of combineReducer (or top level reducer) to be able to share that state.

Also, as of my understanding, the redux principle states that a reducer is a pure function. This doesn't make a reducer impure and, on the other side, by definition, a reducer takes the state and returns a new version of the state. The decision of create reducers that only takes a portion of the state is an implementation detail of the combineReducer approach, which is opinionated and can be replaced. (in Redux)

For your second point, I disagree too. By implementation a reducer can mutate the state and violate the principle. We need to resort to strategies as freezing the state during development to ensure that never happens.
What's the difference on allowing a reducer to directly mutate its slice of the state and allowing it to mutate any part of the state? This is a situation a developer has to consciously avoid.

This is a valid use-case that I haven't seen being addresses by ngrx (that can be addresses by using redux directly). I have seen workaround that are much worst than the problems you suggest it will create.
BTW, do you have any suggestion on how to solve this?
My app has settings, the settings are loaded on the store. Some settings are shared between modules, such settings are known as global settings. How can I, from a feature reducer, access one of these settings?

Thanks,
Robert

While I agree with your points, I'm afraid this will be confusing and will often be miss-used.

The suggestions to solve this, are more or less the same with the Redux docs:

  • add the settings to actions
  • override the default combineReducers method with the reducerFactory configuration option
  • create one "big" reducer containing multiple case reducers or wrap it inside a meta reducer
  • re-think the overall state

I'll re-open the issue to see what others have to say.

Hi @RobertMonks,
I completely agree and understand what your need. And I also share the @timdeschryver concern that we should not inject root state into feature reducer otherwise we lost the authority part and any one can update root state directly.

However, in order to solve global appsettings concerns there are various options to solve it. In our project we did something like this. When APP is initialized and we receive appsettings. From the root module, we dispatch one action saying APPInitializedAction and in that action we put appSettings. Each Feature module subscribes for this APPInitializedAction and each feature cherry picks their desired data from the appSettings coming from the APPInitializedAction action and they store it in their state. Hence in their feature reducer state they already have those data which they need. In some of the features state I also stored the entire appsettings because I was not sure what I may need. However, the idea is to store the global supplied value in feature module state via Action subscription so that they have the data which they need in Reducer.

Similarly when user is logged in we dispatch action UserLoggedIn and each feature subscribes this action and they get userid saved in their states also.

I am sorry for long description but I hope I was clear !

@timdeschryver Thanks for reopening it ๐Ÿ‘

  • I haven't thought about that. Although it won't cover every scenario in which a reducer will need to query (only read) a property from another part of the state. And I think that can lead to over complicated scenarios too in which I have to delegate the responsibility of accessing the required portion of the state to the components/effect.

  • As far as I've tried, that won't work for feature reducers. I tried to use the reducer factory configuration option and the feature reducers always are wrapped around a reducer that has the list of parameters hard coded.

@roopkt What about feature reducers part of feature modules that might not be loaded at the time that action is dispatched?

Finally, I do agree with all of you that are worried about

we lost the authority part and any one can update root state directly.

My fear is that from all the workaround I've seen on the web, they end being more dangerous, like using a meta reducer, or re-dispatching actions when they are not actually required or moving that responsibility to effects/components to properly populate actions.

In my own mind. If by definition a reducer takes a part of the state, and returns a new one. It should not be wrong to make a reducer access the root state. That's what combineReducer does.
At the very least, we should allow the reducerFactory to replace the featureReducer for lazy loaded reducers.

Hi @timdeschryver ,

Thanks for sharing this question, I never thought of Lazy Loaded module situation since we did not have in our project.

I think of one option, although I never tried out this option practically. In this kind of project where module could load dynamically. Can we feed appsettings as a data to the feature module via angular routing data resolver or any kind of dependency injection technique like token etc and then It will be the responsibility of the feature module to dispatch local AppSettingsFoundAction from module, so that feature module reducer gets it and stores it in state.

The Idea I have is, it is not the global state who is responsible for global data always. Child Module should take some responsibility to fetch the required global settings or shared session data via PUB SUB way. When Feature module bootstraps at that time It can get shared data and dispatch locally as Action such that it goes into the feature state. Therefore, we should not put these data like appsettings etc in Global Redux State only. Rather dispatch appSettings as independent action from Root / Feature module such that each state will get its own piece.

It sounds like you're trying to make a decision in your feature reducer based on the global app settings. Instead of making the decision in the reducer, can you push that decision off to a selector that combines your feature state with the global app settings?

@brianmcd

I think we are suggesting workarounds and the issue with a workaround is that it usually solves a particular instance of a scenario but does not addresses the root cause of it, meaning that, in the future, more instances appear and tweaks need to be done to the hack to cover the new case.

As of my understanding the root scenario is:
"As a reducer, I determine the new state based on the type of action, the payload of the action and the previous state".

The combineReducer was introduced to remove the complexity of having a huge unique reducer that manages every piece of the application for every dispatched action.
Let's remember this 'huge and unique reducer' receives the whole state, all of the actions and is supposed to return the whole new state.
Conceptually, there's no reference to only returning a new 'sub state' or querying only a 'sub state'.
CombineReducer's goal is to remove the complexity of this huge switch/case function for complex applications but it should not reduce its capabilities.
This implementation makes 2 assumptions:

  1. A reducer will only have to produce a new slice of the state to which it is registered.
    It won't ever have the need to modify other parts of the state.

  2. A reducer will only need to know the current state of its slice of state. It won't ever have the need to know other properties of the state outside its feature state.

I've witnessed myself several scenarios on which each one of the assumptions does not apply..

Number 1 is easy to overcome, every reducer receives every action dispatched by the application, so, although not ideal, I can move featureA's logic into featureB's reducer to react to a featureA's action that modifies the featureB's state.

Number 2 cannot be easily overcome and there is where the workarounds I've found start to get out of control.
I've seen people making the whole state available on demand (as a global variable) that is updated using a meta-reducer.
Which is the best option I've found so far and the only one that brings back all the capabilities that a reducer should have.
I've seen people making effects to add the entire state (or the required parts of it) as an action payload during action dispatch.
etc...

It makes me wonder, among other things, why is it OK for featureA's reducer to be invoked in response to featureB's action (including global or ngrx specific actions).
But then I find so many inertia for the idea of allowing a reducer to query the whole state.
Can it mutate it? Yes of course! Just as it can mutate its own state or the action payload and screw the application.
Can it return it instead than the supposed feature state? Yes of course! But eventually it will throw several exceptions by being treated as a feature reducer by itself and some cyclic dependencies.
In general, nrxg (or redux for that case) does not protect us from the developer not knowing about the concepts of a reducer.

Thanks!

I agree with @timdeschryver's explanations that this would cause more complexity than value.