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’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

Closed
jeloi opened this issue Sep 18, 2015 · 31 comments
Labels

Comments

@jeloi
Copy link

jeloi commented Sep 18, 2015

In the Reducer docs it says:

Sometimes state fields depend on one another and more consideration is required, but in our case we can easily split updating todos into a separate function..

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.

@jeloi
Copy link
Author

jeloi commented Sep 18, 2015

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.

@timdorr timdorr changed the title Question: Best practice for updating dependent state across different branches of the state tree? Best practice for updating dependent state across different branches of the state tree? Sep 18, 2015
@gaearon
Copy link
Contributor

gaearon commented Sep 18, 2015

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 };
}

@gaearon gaearon closed this as completed Sep 18, 2015
@melnikov-s
Copy link

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 combineReducers function. Doing so would facilitate this very issue though I'm not experienced enough with redux to have any sense as to whether it would be a good or bad idea. Thoughts?

@timdorr
Copy link
Member

timdorr commented Sep 21, 2015

It's got its own section in the docs, so I think it's pretty prominent personally. 😄

You don't have to use combineReducers if you don't want to. It's a useful utility, but not a requirement. Perhaps reduce-reducers or redux-actions is more your style?

@melnikov-s
Copy link

If the data can be computed from a “lesser” state, don't hold it in the state.

It's got its own section in the docs, so I think it's pretty prominent personally.

Ok, I think it finally clicked for me. Thanks!

@skortchmark9
Copy link

"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?

Mistereo added a commit to Mistereo/react-redux-starter-kit that referenced this issue Dec 6, 2015
This change allows us to write reducers which depend on additional data as described here: reduxjs/redux#749 (comment)
@neverfox
Copy link

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 reduce-reducers to combine the reducer produced by combineReducer with your cross-cutting reducer:

export default reduceReducers(
  combineReducers({
    router: routerReducer,
    customers,
    stats,
    dates,
    filters,
    ui
  }),
  // cross-cutting concerns because here `state` is the whole state tree
  (state, action) => {
    switch (action.type) {
      case 'SOME_ACTION':
        const customers = state.customers;
        const filters = state.filters;
        // ... do stuff
    }
  }
);

@gastonmorixe
Copy link

@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?

@gaearon
Copy link
Contributor

gaearon commented Dec 23, 2015

@imton

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 shopping-cart example in the repo for this approach.

@neverfox
Copy link

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.

@gaearon
Copy link
Contributor

gaearon commented Dec 26, 2015

For computing derived data we suggest using selectors rather than storing it in the state tree.

http://redux.js.org/docs/recipes/ComputingDerivedData.html

@neverfox
Copy link

Which is great and useful, until it needs to be part of state.

@gaearon
Copy link
Contributor

gaearon commented Dec 27, 2015

What do you mean by "needs"? Can you describe a specific scenario you're referring to?

@ryanvanoss
Copy link

@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 password state property:

  • Are there 3 or more consecutive characters?
  • Is there a space in the password?
  • Is the username part of the password?

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 username and password, to simply hold the strings values, and 1 for notification, which handles the page level error notification message and type:

// 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 notification reducer needs to know about the username and password. We can access the password since we're concerned with the ENTER_PASSWORD action type which includes it, but, as it's currently organized, I wouldn't get username and password together. In theory I should also be checking for a ENTER_USERNAME action type to make sure that if they've already entered a password and then return to the username field and modify the username, then I check to make sure that the password does not contain the updated username... But we can skip that for now.

Thoughts?

One solution would be to group username and password together as credentials, and merge our ENTER_PASSWORD and ENTER_USERNAME action types into a single ENTER_CREDENTIALS dispatch, but for shits and giggles, let's say that that's not feasible in this situation.

@gaearon
Copy link
Contributor

gaearon commented Jan 11, 2016

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.

@ryo33
Copy link

ryo33 commented Jun 19, 2016

I created combineSectionReducers.
It solves this in a way like combineReducers' one.

@nivek91
Copy link

nivek91 commented Jun 24, 2016

Maybe know something about this? @gaearon omnidan/redux-undo#102

Thanks!

@jalooc
Copy link

jalooc commented Jul 27, 2016

@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 combineReducers with state branches: chat and ui. Inside of the chat-specific reducer we need to react to an action (e.g. received fresh msgs from server) in a way depending on a state from ui part (e.g. append the fresh msgs only if the end of the msgs list is in the viewport - that info is kept in the ui part ). We cannot acces state from ui branch inside the chat branch - therefore it's impossible to use a selector.

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?

@markerikson
Copy link
Contributor

@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 combineReducers".

I'd definitely be interested in further feedback on the draft doc page and this topic in general.

@harshayburadkar
Copy link

harshayburadkar commented Oct 25, 2016

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.
Also I think it is highly suggested to use lean state, and usual recomputation and first see if you have performance issues. Very important. Only use the reselect approach if you see performance issues from recomputation (yes reselect can be applied incrementally, right @gaearon ?). I did take the approach and noticed that the recomputing did not affect at all which I was fearing will surely happen but it never did, on the other hand I am able to sort items at every key press and the app feels blazing fast even without using reselect. People many a times tend to think too much about the steps of recomputation but please never forget today's average processors can perform hundreds and thousands of sorts in fraction of a second. True story.

@funwithtriangles
Copy link

funwithtriangles commented Apr 1, 2017

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 stateInjectedReducers for any new reducers that need injecting with state from elsewhere.

@markerikson
Copy link
Contributor

@funwithtriangles
Copy link

@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 dog, cat and frog state slices were too intertwined to be separate reducers and merged them into one! 😛

@augbog
Copy link
Contributor

augbog commented Jun 26, 2017

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

const combinedReducers = combineReducers({
  a: aReducer,
  b: bReducer
})

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 combinedReducers anyways and it ends up making the state change in both trees. The only issue is that you have to dig through and see if certain actions are trigger changes in different state trees (but we can use a naming convention to help handle that).

@funwithtriangles
Copy link

@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.

@markerikson
Copy link
Contributor

@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.

@jameslockwood
Copy link

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.

@mfp22
Copy link

mfp22 commented Jul 28, 2017

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:

  1. State tree should not contain anything which is derived from anything else in the state tree.
  2. Selectors should be used to compute derived data.

As far as forms are concerned, to me you have 2 things that belong in the store:

  1. Validation rules
  2. Data from user input (text, blur, other events)

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.

@mabedan
Copy link

mabedan commented Oct 26, 2017

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.

function incrementIfOdd() {
  return (dispatch, getState) => {
    const { counter } = getState();

    if (counter % 2 === 0) {
      return;
    }

    dispatch(increment());
  };
}

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.

@natkuhn
Copy link

natkuhn commented Dec 29, 2017

It's got its own section in the docs, so I think it's pretty prominent personally.

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

@markerikson
Copy link
Contributor

Yep, that's where it's moved to.

@reduxjs reduxjs deleted a comment from mileschristian Mar 9, 2018
@reduxjs reduxjs locked and limited conversation to collaborators Mar 9, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests