-
-
Notifications
You must be signed in to change notification settings - Fork 15.3k
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’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Best practice for updating dependent state across different branches of the state tree? #749
Comments
Sorry, the other option I've discovered is using a custom rootReducer. Technically, this reducer wouldn't have to be at the root; it could just be the Lowest Common Ancestor of the parts of the state tree that are dependent on each other. |
If the data can be computed from a “lesser” state, don't hold it in the state. Use memoized selectors as described in Computing Derived Data. If it cannot, indeed, use a single reducer that may (if desired) pass additional parameters to the reducers or use their results: function a(state, action) { }
function b(state, action, a) { } // depends on a's state
function something(state = {}, action) {
let a = a(state.a, action);
let b = b(state.b, action, a); // note: b depends on a for computation
return { a, b };
} |
Thanks @gaearon for that snippet. I've quickly ran into this issue myself when attempting to port a widget over to redux and have spent sometime trying to find a community sanctioned solution. Perhaps this is something that can be more prominent within the documentation as I'd imagine it's rather common to have dependent state. It also appears that it would be trivial to pass the entire state tree as a optional third parameter within the innards of the |
It's got its own section in the docs, so I think it's pretty prominent personally. 😄 You don't have to use |
Ok, I think it finally clicked for me. Thanks! |
"If the data can be computed from a “lesser” state, don't hold it in the state." What if the data can only be computed from a "lesser" state combined with the payload of an action? |
This change allows us to write reducers which depend on additional data as described here: reduxjs/redux#749 (comment)
When you need different parts of the state tree to resolve the effects of single action that cannot be handled in a mutually exclusive way in each branch, use
|
@neverfox Thank you for that! exactly what I was looking. Do you know any other way of either accessing the root state from the combined reducer or re-architecturing something? |
For your particular example I'm not sure why don't want to let different reducers to handle the same actions. That's pretty much the point of using Redux. Please see https://gist.github.com/imton/cf6f40578524ddd085dd#gistcomment-1656424. I don't think “cross-cutting” reducers are generally a good idea. They break encapsulation of individual reducers. It's hard to change internal state shape because some other code outside reducers might rely on it. On the opposite, we recommend not relying on the state shape anywhere except reducers themselves. Even components can avoid relying on the state shape if you export "selectors" (functions that query the state) from the same files as reducers. Check out |
To be clear, I was only suggesting the cross-cutting reducer for cases that aren't mutually exclusive, viz. cannot just be handled by separate reducers responding to the same action. For example, an action comes in and it needs to take some data from one branch of the tree (normally handled by reducer A) and some data from another (normally handled by reducer B) and derive some new state data from the combination of them both (perhaps to be stored in another state branch C). If there's a way to do this short of refactoring the state shape or duplicating data (which isn't always feasible or desirable), then I'm certainly open to suggestions, because that kind of situation comes up a lot as apps grow and new features come along that didn't play into the original state design. |
For computing derived data we suggest using selectors rather than storing it in the state tree. |
Which is great and useful, until it needs to be part of state. |
What do you mean by "needs"? Can you describe a specific scenario you're referring to? |
@gaearon, curious to hear your thoughts on the following scenario... As you enter chars in a password field, we run the following validation rules against the
If any of the above is true, we show a page level error with a corresponding specific page level error message and, depending on which error, we may show a field level error message. So I'll have 3 reducers, 1 each for // reducers/notifcation.js
import { ENTER_PASSWORD } from "../../actions/CreateAccount/types";
import strings from "../../../../example/CreateAccount/strings_EN";
const DEFAULT_STATE = {
type: '',
message: ''
};
export default (state = DEFAULT_STATE, action) => {
const { value: password, type } = action;
if (type === ENTER_PASSWORD) {
const errors = strings.errors.password;
let message;
if (password.length <= 3) {
message = errors.tooShort;
} else if (password.indexOf(username) !== -1) {
message = errors.containsUsername;
} else if (password.indexOf(' ') !== -1) {
message = errors.containsSpace;
}
if (message) {
return {
message,
type: 'error'
};
}
}
return DEFAULT_STATE;
}; So in this case, the Thoughts? One solution would be to group |
Please take a look at redux-form and how it handles scenarios like this. In general, it's better to ask questions on StackOverflow than here. I'm currently unable to give long answers because I'm busy. |
I created combineSectionReducers. |
Maybe know something about this? @gaearon omnidan/redux-undo#102 Thanks! |
@gaearon The discussion with @neverfox stopped abruptly, but I think I know what he meant - I came across the same problem: Let's say we have a root reducer composed using The first solution that came to my mind was to do the conditional part in a dedicated thunk and from that place dispatch one of two dedicated actions. This however introduces several additional creations and therefore quite badly obfuscates what's going on. For this reason I find @neverfox's solution the cleanest one, although, as you said, it breaks encapsulation. Do you have other opinion/solution on this matter after stating the problem as I did above? |
@jalooc : The Redux FAQ discusses this issue, at http://redux.js.org/docs/FAQ.html#reducers-share-state. Basically, the main options are "pass more data in the action" or "write reducer logic that knows to grab from A and pass to B". I definitely feel that @neverfox's approach is the best way to handle it if you opt for the reducer-centric approach. I'm also in the middle of writing up a new set of docs on structuring reducers, over at #1784 . One of the new pages specifically addresses this concept - "Beyond I'd definitely be interested in further feedback on the draft doc page and this topic in general. |
Whatever people say here. I highly suspect the problems people usually do face are only when they architect their state in a non ideal way. If there is a lot of dependency among your branches in the state tree it itself might be an indication of duplicated data and can use 'lesser' / 'leaner' state. React+Redux platform is giving you immense power in your hands and highly deterministic UI updates. Those who haven't suffered the problems of weird UI updates and loops of 2 way bindings probably will not understand this. When there is a lot of power in your hands inherently means you can easily shoot yourself in the foot too which will be a nightmare and feel every one's pain too. Once you get the state architecture part right everything else will become easy. So basically you need to spend more time in state architecture but you can reap benefits all throughout after that. |
Has anyone considered this approach? What are the pitfalls? const combinedReducers = combineReducers({
dog: dogReducer,
cat: catReducer
})
const stateInjectedReducers = (state, action) => {
return {
...state,
frog : frogReducer(state.frog, action, state) // frogReducer depends on cat state
}
}
export default reduceReducers(combinedReducers, stateInjectedReducers) It's cheap and easy but appeals to me because it means I won't be having to rewrite |
@funwithtriangles : yup, perfectly good approach! In fact, I talk about that in http://redux.js.org/docs/recipes/reducers/BeyondCombineReducers.html , and my blog post at http://blog.isquaredsoftware.com/2017/01/practical-redux-part-7-forms-editing-reducers/ . |
@markerikson glad to know I wasn't doing anything too crazy! :) The irony is that after coming up with that approach I decided that my |
Just curious is it bad practice to simply have multiple reducers respect the same action and then trigger their own respective changes in their own state trees? I also think its possible if you have to do this, it might be an indicator you have structured your state tree incorrectly but for purposes lets say you have two different states
And you wanted to trigger a state change in both state trees from one dispatch. Is it bad practice to simply have in both reducers respect the same action? Inevitably, the dispatch goes through the whole |
@augbog That's totally fine and is a common thing to do in Redux, but isn't really related to what is being described above. This issue is about reducers having access to the state from other branches of the state tree. |
@augbog : that's absolutely an intended use case for Redux! For more info, see my blog post The Tao of Redux, Part 1 - Implementation and Intent. |
Another angle - Use a selector at the root of your state tree to derive the particular data you need by calling selectors from sub-branches of the state to get necessary data, then return the data shape you require: const selector = state => {
const x = someSelectorForA(state.a);
const y = someSelectorForB(state.b);
return compute(x,y);
} Your reducers remain encapsulated, no race-conditions, and the selector is doing the grunt work. Can lead to some nice recursive patterns. |
Whenever I have been tempted to reach across branches of the state tree, it usually is when I should be using a selector. I try really hard to follow these principles:
As far as forms are concerned, to me you have 2 things that belong in the store:
Then you use a selector to compute whether 2 follows the rules defined in 1. There's no reason to reach across the state tree in a reducer. I've sometimes thought, "Doesn't it seem like whether a form is valid or not should be reflected in the application state? It seems like a major thing to keep track of." But still, it violates the principle that state should be the single source of truth. If there is a bug where validity is computed incorrectly, your state will be internally inconsistent. That should be impossible. Selectors are perfectly good places to store derived data, even if the results are major. Use a memoized selector as @gaearon suggested, because the computed state may have many interested subscribers. And if your reducer needs the contextual information of derived data in order to interpret an action correctly, you can just include that with the action when you dispatch it. |
An alternative to this problem, is using redux-thunk. The solution is to use intermediate actions, to fire more context-aware actions based on the current state.
Using this solution, both actions and reducers stay dumb, and the logic moves to an intermediate layer which does the extra checks and deductions based on user interaction and current state. |
The link with this quote is broken, but I believe it refers to this: https://redux.js.org/docs/recipes/ComputingDerivedData.html On second look, though, this section looks like it may be as relevant if not moreso: https://redux.js.org/docs/recipes/reducers/BeyondCombineReducers.html |
Yep, that's where it's moved to. |
In the Reducer docs it says:
I'd like to know how to handle that specific use case. To give an example, say I have a form with multiple data fields. I have another component elsewhere that should react to changes in form, e.g change state based on whether the form data is valid, or compute values from different inputs to the form and display them.
While I believe I can have different reducers (which handle different slices of the state tree) listen to the same action and update themselves, the "reactive" state would not have access to the other branches of the state tree that it needs in order to compute itself.
One solution I've thought of is to have the "reacting" process happen by using subscribe to listen to changes in the state tree, compute whether or not a relevant change was made, then dispatch a new action to update the dependent fields. However, this publishing blast might have to become very generic, so that all reducers that care about it will be able to use the payload it comes with.
I'm new to Redux, so I'm not sure if there is an adopted pattern for this type of thing. I'd appreciate any advice or pointers in the right direction. Thanks.
The text was updated successfully, but these errors were encountered: