Redux: Best practice for updating dependent state across different branches of the state tree?

Created on 18 Sep 2015  ·  31Comments  ·  Source: reduxjs/redux

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.

question

Most helpful 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 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
    }
  }
);

All 31 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 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?

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

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?

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!

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

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

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

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

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.

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

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

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.
It solves this in a way like combineReducers' one.

Maybe know something about this? @gaearon https://github.com/omnidan/redux-undo/issues/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 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?

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

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.

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.

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

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

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

  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.

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.

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

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

benoneal picture benoneal  ·  3Comments

parallelthought picture parallelthought  ·  3Comments

captbaritone picture captbaritone  ·  3Comments

timdorr picture timdorr  ·  3Comments

dmitry-zaets picture dmitry-zaets  ·  3Comments