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 to produce
, 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 a produce
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
.
// counter.js
const reducer = (state?: State = initialState, action: Action): State => {
switch (action.type) {
case ActionTypes.INCREMENT:
state.count += action.payload
break
case ActionTypes.DECREMENT:
state.count -= action.payload
}
return state
}
export default produce(reducer, initialState)
This is the root reducer which has a slice of state handled by counter.js
. The counter
reducer should be pure, but when it's called within a produce
call, which it is, it loses it's purity.
// reducer.js
const reducer = (state?: State = initialState, action: Action): State => {
switch (action.type) {
case ActionTypes.TOGGLE:
state.value = !state.value
}
state.counter = counter(state.counter, action)
return state
}
export default produce(reducer, initialState)
Instead, reducer.js
has to be modified to avoid recursive produce
calls:
// reducer.js
const reducer = (state?: State = initialState, action: Action): State => {
switch (action.type) {
case ActionTypes.TOGGLE:
state.value = !state.value
}
- state.counter = counter(state.counter, action)
return state
}
-export default produce(reducer, initialState)
+export default (state?: State, action: Action): State => {
+ const nextState = produce(reducer, initialState)(state, action)
+
+ return { ...nextState, counter: counter(nextState.counter, action)
+}
Proposal
Enable recursive calls of produce
by default like pre-v1.3.0. Add a new method setRecursiveProduce
(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.
proposal