-
-
Notifications
You must be signed in to change notification settings - Fork 849
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
Idea: Ignore return value of producer #246
Comments
IMO: Warn when produce(draft => {
draft.a = 1
return draft // This line is required when you mutate `draft`
})
produce({}, draft => undefined) // => undefined |
The |
I really like how the "return the draft" requirement discourages footgun one-liners: // The footgun
produce({}, draft => draft.a = 2) // => 2
// When the draft must be returned, safe one-liners are more intuitive.
produce({}, draft => (draft.a = 2, draft)) |
@aleclarson do you mean in the current, or new situation? The proposal here is to never care about what is returned, so the above could also be written as |
I've also being bitten a couple of times by: produce({ x: 1 }, draft => draft.x = 2) // => 2 If you want to replace a state why not call produce with the new state produce({ x: 1 }, { a: 2 }); As the function is kind of superfluous. If you do so, you can go with always ignoring the value returned by the function: produce({ x: 1 }, draft => draft.x = 2)
// returns { x: 2 } |
@albertogasparin There's no point in using |
@mweststrate My proposal is separate from yours.
My proposal is the opposite: "The return value is always used" I think explicit returns are the easiest to reason about. And no need for a |
@aleclarson How do you avoid erroneous returns? As you pointed out, even if we say that the returned value is used as new state, arrow function still makes it very error prone: // The footgun
produce({}, draft => draft.a = 2) // => 2 I like more the main proposal (ignoring the return), which will let immer care only about the changes made to @mweststrate Besides I don't understand why would someone to use immer to replace the state with |
Yes, producers may use conditional logic to determine if no changes, some changes, or a replacement is appropriate. Is optimizing for one-liner producers really worth it? Are people using one-liners more often than they are replacing the base state? I'm really not a fan of adding a edit: I think it's simpler for developers to learn "the return value of a producer is always used, just like in a reducer" instead of "the return value is only used if you wrap it with a |
Provoked by: #246 Goal: Simplify the mental model of producers Hypothesis: When `produce` returns exactly what its recipe function returned, complexity is reduced for developers (and TypeScript)
Provoked by: #246 Goal: Simplify the mental model of producers Hypothesis: When `produce` returns exactly what its recipe function returns (apart from proxies, of course), complexity is reduced for developers (and TypeScript) BREAKING CHANGE
BREAKING CHANGE Provoked by: immerjs#246 Goal: Simplify the mental model of producers Hypothesis: When `produce` returns exactly what its recipe function returns (apart from proxies, of course), complexity is reduced for developers (and TypeScript)
To summarize, the options are: Updating a value (the common case)
Replacing a value (the non-common case)
Replacing state with
|
My vote is to keep the current behavior. Note: The import {produce, replace} from 'immer'
produce({ count: 0 }, draft => draft.count++)
// => { count: 1 }
produce({ foo: true }, draft => ({ foo: false }))
// => { foo: false }
produce({ a: 1 }, draft => {
draft.a = 2
return replace({}) // Throw an error if `replace` is not used
}) // => {} Also, there could be a special case for produce({ a: 1 }, draft => {
doSomeMutations(draft)
return original(draft) // Rollback to original state
}) // => { a: 1 } |
Yeah, that is something I can live by as well :)
I think that will be too error prone / subtle. The result of produce will depend on unclear ways on the code paths taken in the produce function itself. (we could otherwise apply this already to the current implementation; only inspect return value if draft not modified). |
I agree with @aleclarson on this. I also agree that the void trick should be removed from the docs. But I disagree that we should always return the draft. because that would confuse someone looking at it for the fist time. right now, what we have is perfect. just remove void trick. I have the following justifications:
|
Also, we should NOT warn for non-undefined returned values: // this will return 1 and warn, we don't want that. assignment expressions return the assignee, remember
const newState = produce(state, draft => draft.a = 1); In any case, I have another proposition. Just another similar proposal: import produce, { replaced } from 'immer';
// -> {blah: 1}
const newState = produce(state, draft => {
return replaced({blah: 1});
});
// -> {blah: 1}
const newState = produce(state, draft => replaced({blah: 1}));
// -> undefined (remove nothing heler)
const newState = produce(state, draft => replaced());
// -> null
const newState = produce(state, draft => replaced(null));
// -> 1
const newState = produce(state, draft => replaced(1));
// {a: 1} , returned value is ignored
const newState = produce(state, draft => draft.a = 1); Slightly more readable that calling import produce, { replaced, isReplaced } from 'immer';
const newState = produce(state, draft => {
const val = functionWorkingWithDraft(draft);
if (isReplaced(val)) {
return val;
}
// continue
}); I'm not very sure about this, but at the very least, it's a bit more readable |
That is actually the reasoning behind this proposal. Conceptually, return values from recipes don't make any sense / are meaningless; they are to mutate the draft. The only reason why evaluating the return value was added, later on (in 1.1, commit), was to support the typical redux case of returning new stuff. Which in turn introduced the work-arounds of So by going back to the original behavior, immer / producers themselves get cleaner, except for the exceptional redux case, where it is made more explicit that intentionally a new state is returned, by using |
@alitaheri ehh.... I think, beyond the name, that is exactly the current proposal? |
@mweststrate no, it's not. it's a bit different I'm saying any return value must be ignored EXCEPT when it's the result of produce({ x: 1 }, draft => {
replace(3)
}) vs produce({ x: 1 }, draft => {
return replaced(3)
}) this way, if a nested function returned having a function that changes the behavior of a whole subtree is an anti pattern, a framework smell, and we hard to compose. |
@alitaheri ok, lol, what you are saying was intended to be the proposal. Will update the original post, the example code is broken :'). Edit: well, the warnings are different indeed then original proposed. But I agree, and removed them from the proposal. |
Poll results (102 votes):
Seems worth pursuing in a MR |
/cc @knpwrs @jineshshah36 @markerikson Your opinions would be valuable! |
I would definitely agree with ignoring the return value. However, I would be curious to know what the return type of replace is as well as the type of nothing. To make this design the most type safe, I would consider doing the following:
|
Coming from the Redux point of view, I would definitely be against ignoring the return value, for two reasons:
export function createReducer<S = any, A extends Action = AnyAction>(
initialState: S,
actionsMap: CaseReducersMapObject<S, A>
): Reducer<S> {
return function(state = initialState, action): S {
return createNextState(state, (draft: Draft<S>) => {
const caseReducer = actionsMap[action.type]
return caseReducer ? caseReducer(draft, action as A) : undefined
})
}
} From the end user's point of view, there's no visible evidence that Immer is being used: const todosReducer = createReducer([], {
"ADD_TODO" : (state, action) => {
// "mutate" the array by calling push()
state.push(action.payload);
},
"TOGGLE_TODO" : (state, action) => {
const todo = state[action.payload.index];
// "mutate" the object by overwriting a field
todo.completed = !todo.completed;
},
"REMOVE_TODO" : (state, action) => {
// Can still return an immutably-updated value if we want to
return state.filter( (todo, i) => i !== action.payload.index)
}
} Removing the ability to return a new value would cause us issues. We do have some warnings in our RSK docs about the fact that we're using Immer, that it's what makes the "mutating" possible, and the caveats around both mutating and returning a new value, so the abstraction is admittedly a bit leaky to begin with. But, I'd prefer not to have to force users to explicitly rely on Immer's API for replacement behavior. Then again, I suppose we could make tweak our |
@markerikson You could call |
I changed knpwrs/redux-ts-utils to use |
@markerikson Raises a good point. I have an idea. We can't just warn by default since cases like this: return produce(state, draft => draft.a = 2); will warn for no good reason. However, we can add an opt-in options: calling I'm still not 100% bought in the idea, but the community agrees that we should go down this road, so let's at least enable gradual migration paths. P.S. the warning must use |
@jineshshah36 I think we could overload the produce function. if it matches |
@alitaheri the point of typing Return as |
@jineshshah36 oh, we shouldn't enforce that. because of this: return produce(state, draft => draft.a = 2); the assignment expression evaluates to 2 and the arrow function returns it. so if we type it in a way that disallows return type of |
Typewise it is not very hard to make that work |
@markerikson thanks for the clarification, that is really insightful! I think the abstraction leaks in any case indeed, but I can imagine that you don't want to explicitly use an api of immer for replacement. (Let's throw replacement values wrapped in an exception, lol :P) |
Ok, closing this for now. So far everybody seems to be getting along with Either:
I think in hind-sight, replacing the returned state should not have been supported in the first place (it's not the point Immer, just made currying for reducers more easy), which leaves it to the wrapping abstraction to deal with those cases (need replace? don't call |
In react I am doing:
But then as I am doing changes to the draft, I realize I want to cancel the setState by returning I also have this problem with hooks:
|
Afaik that should work OOTB. Code you create a sandbox demonstrating the
issue?
Note that it is not possible to return undefined from a producer, in that
case, one should return `NOTHING` (can be imported from immer)
…On Wed, May 8, 2019 at 4:22 AM Noitidart ***@***.***> wrote:
In react I am doing:
this.setState(produce(draft => {
});
But then as I am doing changes to the draft, I realize I want to cancel
the setState by returning null. Is it possible to do this somehow? As
returning null in setState cancels.
I also have this problem with hooks:
const [ pastes, setPastes ] = useState([]);
setPastes(produce(draftPastes => {
const draftPaste = draftPastes.find(paste => paste.id === pasteId);
if (!draftPaste) return null;
draftPaste.comments.push(comment);
}));
—
You are receiving this because you modified the open/close state.
Reply to this email directly, view it on GitHub
<#246 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAN4NBE26QAXC6FF6IAR6CDPUI2MXANCNFSM4GCZPBDA>
.
|
Currently the return value (if any) matters a lot, to be able to replace the current state. So there are quite some rules
undefined
(or noreturn
at all), will assume thedraft
is the state to be returneddraft
orundefined
), will replace the state with the returned valueThis cause a few tricky cases:
undefined
, by simple returningundefined
. For that reason,nothing
has to be returnedproduce({ x: 1}, draft => draft.x = 2)
will produce2
as the next state, not{ x: 2 }
. (Since assignment is statements return their right hand value, the result of the function is2
, notundefined
. To avoid this, we have to write eitherdraft => { draft.x = 2 }
ordraft => void draft.x = 2
.We could design the API differently, and always by default ignore the return value of a producer, regardless what the return value is. This fixes point 2, where we accidentally replace the state.
To be able to replace the state, we could have a dedicated internal indicating that we intend the replacement:
return replace(3)
. This also solves 1), asundefined
has no longer to be treated specially, and we can justreturn replace(undefined)
.Note that we could even detect a
replace
call withoutreturn
and warn the user.Beyond that, we can possible warn about return a value, but not updating the draft (probably replace was intended)
So, in summary:
Alternatively we could warn about non-
undefined
return values (fixing the last case) and forcing consumers to either used => { ... }
ord => void ...
ord => replace(...)
in producers.Edit: removed the warnings
The text was updated successfully, but these errors were encountered: