-
-
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
Feature Request: Allow reducers to consult global state #1768
Conversation
This has been suggested numerous times so we probably should consider it.
If you wire things like this by hand right away instead of using |
I just changed it to include the new state because part of me agrees with you. But now after thinking about it, I'm wondering if only passing the original global state makes sense as a way of enforcing the spirit of paths. In other words, reducers shouldn't really rely on the state changes on other paths caused by previous reducers. I could be sold either way. Your second point is valid. In my current implementation it's not a "global" state. If you used If you wanted to pass the true global state to a reducer and still allow for multiple levels, you'd need a version of
Using this would actually be a more declarative way of expressing multiple levels of reducers than just manually applying If you like this idea I can expand this pull request to implement such a function. |
I feel like part of the issue is that A large part of me says that issues like the nested-tree problem you mention show that that structuring reducer logic is a very app-specific issue, and that modifying Can this be dealt with more at the documentation level? I keep yammering about wanting to put together a "Structuring Reducers" recipe page, and I just haven't had time to do so. The related FAQ question is useful, but I really want to have a much larger page demonstrating actual various approaches to building more involved reducers. I also feel like the |
@markerikson Agreed that |
So as far as this specific PR goes: my personal expectation for such an argument would actually be that the last argument would always be the global state, not one level up, and that it would be the previous state. |
I’m feeling that if we do something like this, we need to update all examples and documentation to use selectors (the regular ones, not Reselect) so it’s clear how to read safely from a global value without introducing crazy dependencies between reducers. |
In the latest commit I implemented the function I was imagining. It should be backwards compatible. @gaearon By "regular selectors" do you just mean selectors implemented manually without an external library? Hmm, taking the idea of selectors one step further, I could also modify
|
Hmm. I could see some use for some of these changes. So if I'm following this correctly, at the moment you're proposing / have implemented three major changes:
I could sorta maybe get behind those changes. Still a couple concerns. First, taking in a nested tree solves some use cases, but as @gaearon said, doesn't help with the specific use case of actually having called const rootReducer = combineReducers({
a : reducerA,
b : combineReducers({
b1 : reducerB1,
b2 : reducerB2
})
}); Then reducer B1 wouldn't actually get the complete state, would it? Does that mean that cases like dynamically adding sub-reducers wouldn't ever be able to get the complete state? The idea of having a selector function as an argument seems sorta-kinda useful, but same potential concern. Second, I still think the additional "complete state" value should be the previous state. I'm not sure I follow the reasoning for wanting it to be the "next state", or even how that's useful. Ah... looks like that approach was removed in the later commits, though, so I may be complaining about something that's now out of date. For reference, here's a few reducer libs that demonstrate some related prior art:
|
Wow thanks for the quick turnaround time. 👍 You mention the use case of calling
It's true that, as I had implemented so for in the PR, if you had setup the reducer like this then However, I see this as generally moot because with
In this case, if you wanted the logic below You're also correct that in this version I pass the previous top-level state to the sub-reducers (rather than the next). You've convinced me that this makes more sense given that we're passing the reducers as an object and object keys aren't ordered. Anyways, it seems dubious to have one reducer depending on changes made by another reducer in the same tick unless you have a grammar for defining dependencies as in https://github.com/KodersLab/topologically-combine-reducers). Otherwise, if someone needed to do that they could just wrap their reducer function and handle that themselves. |
My couple vague-ish concerns at this point are:
I don't have any specific concrete examples or test cases I can point to at the moment, just mental unease. Thoughts? |
One more very relevant piece of prior art: https://github.com/ryo33/combine-section-reducers looks like it's doing just about the same thing as this PR. |
There is no set of passed arguments that would have been valid for the previous version of To support this claim, if I run the implementation of this PR against the specs of the master branch, only one test breaks, which is Or, looking at the code, if you just assume that the argument passed to If all of this is true, then the new implementation is backwards compatible and then both of your concerns are covered. All of the new behavior is essentially opt-in, whether that be: a) accessing the global state from a sub-reducer or b) passing a reducer tree of arbitrary depth to The library you mention (https://github.com/ryo33/combine-section-reducers) is similar in that it allow you to pass the global state to a reducer, but is different in that it doesn't support a reducer tree of arbitrary depth. Also, it doesn't support the same sanity checks on state shape that |
Okay, sounds good. Can we get some doc updates to match the code? |
FYI I haven't forgotten about this. Work has been crazy, but I'm hoping to get to this this coming week. |
Updating combineReducers.md to reflect how `combineReducers` was expanded to support recursive reducer trees.
I updated the docs section on combineReducers. I pondered also updating the section on basic reducers, but I felt like the goal of the section was clearly outline the concept of reducing and that rather than distracting from that I should leave recursive reducer trees as an advanced feature left for the API docs. |
I also was looking at #1792. There's nothing inherently incompatible between those proposed changes and what I've done here. I could wait until someone gets a PR merged for that and then I could merge those into this PR. Or alternatively I could just directly add that feature (which would look something like borkxs@7b3315e but ) into this PR. Either way. |
@eremzeit |
Apologies for the confusion. The difference is that this PR implements
Also, it's implemented in a backwards compatible way into the original In regards to sanity checks, I just meant those that were already included with |
I think we shouldn’t just go ahead and add this because it seems useful in some situations. If we add this, we maintain this forever. It’s a big cost if we later discover there are some problems with this API or if there are bugs to be fixed. I would rather see this as a third-party library. If it’s truly useful, you should have no problem submitting pull requests to boilerplate projects, examples and open source apps that would benefit from it. If it gains enough popularity, we can consider replacing |
I certainly agree that there's more than enough demand out there to consider such a change. The biggest question, as we've seen is what the semantics of the additional argument should look like, and that's where things start getting complicated. I still feel like part of the issue is user education that |
(Note that I do sort of like the API. But I would like to see real world usage before it becomes something we ship to thousands of users.) |
I was actually just discussing this with a coworker this afternoon. In the new product we're building, he's ended up designing a state tree with a very overloaded key in order to cross a lot of boundaries. Unfortunately, Why not fold-in and provide |
@timdorr : that's part of what I'm hoping to clarify with the "Structuring Reducers" doc I'm (intermittently) working on. See #1784 . I'd certainly be interested in any feedback on the intended content, the WIP so far, and actual use cases and approaches people are coming up with. I'm assuming that you're referring to something like an "entities" slice, where it's less "specific subset of domain data" and more "ALL the domain data", hence involved in all kinds of manipulations? What "mistakes" are you referring to? |
Ah, that's a great start! I still think it's a combination of docs and signals via the APIs we provide that will get folks to unlearn bad habits. But I'm about to have a baby in about 36 hours, so my time is going to be really limited. But I'll give it a look-see when I'm back to reality!
Yeah, we have a search widget that was containing both the search criteria and the results data under one key. It was really starting to get messy and my coworker was becoming painfully aware of how he was cargo culting |
@timdorr: Hey, slightly early congratulations, then! I wrote much of the FAQ page during a 3-week business trip back in March. I'm on a 2-week trip right now, so I'm hoping that if nothing else I can sit down this weekend and crank out a pretty good chunk of writing. |
@gaearon , @timdorr , @jimbolla , @eremzeit : This PR has been quiet for the last few months. In the meantime, we've had a couple more PRs filed requesting changes to It seems like we have four major options:
Thoughts? |
I'm a fan of composition vs. expanding an API. If we could break up |
I think there's potential to model |
I'm not really a fan of passing global state because it breaks encapsulation. IMO, combineReducers defines a sub-state (or component) of the application. That sub-state should not know anything about the parent. Using a selector to pass a third argument, whether that be from the top-level state or not, that's a bit more interesting. However, I'm hesitant about that because it breaks the pureness of a I actually abandoned the use of combineReducers in my project in favor of a simple combine function: // TypeScript:
export function combine<T>(a: T, b: Partial<T>): T {
const keys = Object.keys(b) as (keyof T)[]
if (keys.some(k => a[k] !== b[k])) {
return Object.assign({}, a, b)
}
return a
} Which I use like this: const reducer = (state, action) => combine(state, {
a: reducerA(state.a, action),
b: reducerB(state.b, action),
})
// vs
const reducer combineReducers({
a: reducerA,
b: reducerB,
}) It's slightly more code, but overall it was easier to follow, and allows someone to more easily operate on the top-level state if needed. |
@lukescott : a pure function that takes Yes, the intended usage of |
I think that possibility to set in projects only one import combineReducers from './combineReducers'
export default function createRootReducer(reducersTreeNode) {
if (typeof reducersTreeNode === 'function')
return reducersTreeNode
Object.keys(reducersTreeNode).forEach(key => {
reducersTreeNode[key] = createRootReducer(reducersTreeNode[key])
})
return combineReducers(reducersTreeNode)
} Example of usage here (shopping-cart example): https://github.com/Phationmationion/redux/commit/eb899d6681355201e51703e63ecb5ea03e286d30 And just idea: remove combineReducers form exports of Redux and inject createRootReducer here // ...code...
let currentReducer = createRootReducer(reducer) |
@Phationmationion : for what it's worth, there are several existing third-party reducer utilities that do that kind of thing already, and I linked some of them upthread. Also see the Reducers section of my Redux addons catalog. There's also no need to "inject a reducer creator into |
Given that this is no longer part of the 4.0 milestone, that the original author of the PR has disappeared, and that there doesn't seem to be anyone who has expressed an interest in pushing it forward, I'm going to close this PR. If someone actually does want to tackle improving |
@markerikson sorry, I've wrote a big message about |
In
combineReducers
I love the idea of having reducers each map their input state to a specific path and that generally they only are interested in knowing the input state of that same path. However, sometimes there are cases that even inside the reducer that writes state for a specific path that you need to access something from the global state. This would happen if you maintain some global resource cache on the top level of the state (eg. via normalizr) that doesn't need to be duplicated at each key path.If you are okay with this change I can also update the readme.