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
Would it be possible to make produce's recursive noop optional? #225
Comments
If you're using |
@aleclarson, we still want to keep the reducers exported from files pure. This is actually recommended in the README at the end of the Redux example. The issue happens when we try to wrap // counter.js
...
-export default produce(reducer, initialState)
+export default undoable(produce(reducer, initialState))
|
Thoughts on a Just know that you take a performance hit (the effect depends on the context). If you're passing the same draft to 3 nested producers, the draft will be re-created 3 extra times. I assume you're aware of that, though. // curried pure producer
export default produce.pure(reducer, initialState)
// inline pure producer
export default function(state: any) {
return produce.pure(reducer, state)
} |
@aleclarson, I like the And yes, but for our case, if we implement batching it reduces the number of The main motivation for this whole thing is that we wanted to batch expensive and unnecessary // counter.js
...
-export default produce(reducer, initialState)
+export default undoable(produce(enableBatching(reducer), initialState)) As you can see, if we put EDIT: |
I personally prefer |
|
@aleclarson, that would work too! Would you want me to submit a PR? |
Yeah, give it a go. I've no need for this feature, currently. |
So... in my search for finding the right way to do this I found clarity, and a potential bug in immer. After much thought, it dawned on me that // counter.js
...
-export default produce(reducer, initialState)
+export default (state, action) => produce(reducer, initialState)(original(state) || state, action) I could even make my own // producePure.js
// NOTE: This is a naive and incomplete implementation that just serves as an example.
const producePure = (state, recipe) => produce(original(state) || state, recipe) If it can be done in userland, then I think it's not needed as a feature. But... unfortunately
history._lastUnfiltered === history.present // false
original(history._lastUnfiltered) === original(history.present) // true If the target of the proxies are the same, the proxies themselves should be equal by reference. At least if immer is properly keep track of its proxies. In this case it seems like it's not. Am I missing something here? Is this a bug? Would it be possible to use a EDIT: I looked more into it and it does seem that what's being used is just an object to keep track of proxies. This seems to be a bug and could be fixed by using a |
You want We'll need to reuse the "draft identity" between proxies, but not the "draft state". When reusing a draft identity, we'll need to remember the previous draft state, so it can be used again when the newer draft is finalized.
|
That's a nice |
@migueloller this issue is becoming hard to follow, with several ideas being floated and retracted. I suggest to:
|
Oh, that's a good point. Looks like |
@migueloller
That is what Immer should do already (unless the state is not a tree) |
This sandbox shows the issue nice and clearly: https://codesandbox.io/s/xj8rvlxo34
This is a good point. Here's my solution: For the inner
He still needs purity for |
@aleclarson i am not sure whether your example is a good example, would rather have expected: produce(base, draft => {
assert(original(draft) == base);
produce(draft, draft2 => { // pass draft, not base!
assert(original(draft2) == base);
assert(draft == draft2); // succeeds!
});
}); Your example just starts another producer on the same original object, and that draft is not the same, which is looks consistent and any other behavior would be more surprising? Take for example the following: const base = { x: 3 }
produce(base, draft => {
draft.x = 2
produce(base, draft2 => {
console.log(draft2.x) // would expect 3 here, base didn't change after all!
});
}); |
Yes, but when the inner Because we can't treat a nested |
Yes!
That's fair. Once the issue with references is fixed, it should be pretty straightforward to add
That seems to be right! It seems that in this case it works because when composing reducers, the slice of the state that is passed wouldn't have been modified so
Yes I can! As soon as I get the chance I'll set up an issue that explains the desired behavior together with a reproduction.
Yes, the immediate need is gone since I can implement Re #225 (comment): Maybe this? const base = { x: 3 }
produce(base, draft => {
draft.x = 2
produce(copy(draft), draft2 => { // copy(draft).x is indeed 2, but original(draft).x is still 3
console.log(draft2.x) // would expect 2 here, since we have to underlying copy
assert(copy(draft) === copy(draft2)) // succeeds since we haven't mutated draft2 yet
});
}); This would be useful as then we could do: const purify = (impureReducer, initialState) => (maybeDraft, action) => produce(impureReducer, initialState)(copy(maybeDraft), action)
const pureReducer = purify(impureReducer, initialState) Does this make sense? |
@migueloller ehh no. What is |
@mweststrate, yes I mean And for |
How is that different than passing just the I have the impression that I kinda lost track on the actual issue, but I have the impression that the root problem is that you use |
Isn't this only safe in Redux when producers avoid deriving from state mutated by other producers? let base = {}
produce(base, draft => {
draft.foo = producePure(draft, reduceFoo)
draft.bar = producePure(draft, reduceBar)
})
function producePure(state, reducer) {
return produce(original(state) || state, reducer)
} In the above example, the This all assumes that |
If the draft is passed in,
That is correct. |
Unfortunately, pushing the producers inward greatly slows down our application since For this reason, we want to do this If we did EDIT: In our scenario we're doing |
Yeah, so it doesn't have to be entirely pushed inward indeed, just inside the undoable, for which hopefully the same concept holds; that it is only applied after all the batched actions have been executed. Just double checking: |
It does! I just have to make sure I do this in my root reducer: const reducer = (s: State, a: Action): State => {
s = enableBatching((state, action) => {
// Handle actions.
})(s, a)
s.entities = entities(original(s.entities) || s.entities, a)
return s
} And in The reason I changed the topic of this issue to the referential integrity stuff is because I would like to do this: // root.js
const reducer = (s: State, a: Action): State => {
s = enableBatching((state, action) => {
// Handle actions.
})(s, a)
- s.entities = entities(original(s.entities) || s.entities, a)
+ s.entities = entities(s, a)
return s
} And then in const originalize = r => (state, action) => r(original(state) || state, action) I could then just do this: const purifyReducer = compose(originalize, produce) Unfortunately, because of the referential issue, I would have to do this: originalize(undoable(produce(enableBatching(reducer), initialState))) Meaning I can't do |
Closing as there is a workable solution |
@mweststrate, it would be nice to be able to do this without having to use |
@migueloller : just read through this thread. I'm curious what your use case is for dispatching that many actions at once. Could you contact me |
@markerikson, happy to talk! I just sent you a message on Reactiflux. |
I believe this plan of action will fix this issue (though I'm not certain yet), which I plan to carry out once #258 is merged. Basically, we can do some internal trickery to ensure purity without losing @migueloller If you have time to reproduce the issue in a minimal test case, that would be super helpful. |
@aleclarson, that would be great! I'll try to get a repro as soon as I have some time, something I find quite hard to find these days 😅 |
This is a feature request and I'm more than happy to implement it myself if accepted.
Background
After v1.3.0, recursive
produce
calls result in a no-op. @mweststrate explained the pros and cons of this behavior here. We're building a project with Redux and have been taking advantage of immer to reduce boilerplate in our reducers. While optimizing our use of Redux by batching actions to avoid excessive calls toproduce
, we've run into an issue with this behavior.Issue
To reduce boilerplate in our Redux reducers, we implement all of our reducers as impure reducers and make them pure by calling
produce(reducer, initialState)
. Some of our reducers use higher order reducers (e.g.,redux-undo
). Because of Redux's contract, high order reducers will often make the assumption that reducers are pure. Unfortunately, after v1.3.0,produce(reducer, initialState)
will only "purify" your reducer if you're not inside aproduce
call.Our current solution is to avoid calling
produce
recursively, which removes the ability to compose our reducers. @mweststrate's point (ii) in the comment is a great observation, but for us it's not an issue when composing reducers since Redux reducers are pure.Here's a small example showing the desired behavior:
This is a sub-reducer that holds a slice of the entire Redux state. One might want to wrap it in a higher order reducer like
redux-undo
.This is the root reducer which has a slice of state handled by
counter.js
. Thecounter
reducer should be pure, but when it's called within aproduce
call, which it is, it loses it's purity.Instead,
reducer.js
has to be modified to avoid recursiveproduce
calls:Proposal
Enable recursive calls of
produce
by default like pre-v1.3.0. Add a new methodsetRecursiveProduce
(for lack of a better name) that would allow configuration of this behavior like post-v1.3.0.Would love to hear some thoughts on our approach and if other people would find something like this useful. If this is something the maintainers would accept, I would gladly submit a PR.
The text was updated successfully, but these errors were encountered: