Skip to content
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

Unexpected access to getter property in irrelevant plain objects #1012

Closed
2 tasks
zthxxx opened this issue Jan 10, 2023 · 6 comments
Closed
2 tasks

Unexpected access to getter property in irrelevant plain objects #1012

zthxxx opened this issue Jan 10, 2023 · 6 comments
Labels

Comments

@zthxxx
Copy link

zthxxx commented Jan 10, 2023

🙋‍♂ Question

Here is two question, when will a plain object contain drafts in the recursively finalize function?

I think a plain object here always means not modify in produce() and not create by immer proxy, maybe move a proxy draft property into new object then add it back to draft's new property field?

image

// A plain object, might need freezing, might contain drafts
if (!state) {
each(
value,
(key, childValue) =>
finalizeProperty(rootScope, state, value, key, childValue, path),
true // See #590, don't recurse into non-enumerable of non drafted objects
)
return value
}

Is that the only thing need to do in this condition for plain object is maybeFreeze(rootScope, value, true) rather than each(finalizeProperty(...))) ?

I think this is the reason to cause some UNEXPECTED access to getter property in an irrelevant plain object when produce() returns result, which will have some side effects.

The problem case like below:

image

Link to repro

To reproduce: https://stackblitz.com/edit/node-pgmcfz?file=immer-getter-without-freeze.mjs

Environment

  • Immer version: 9.0.17
  • Occurs with setUseProxies(true)
  • Occurs with setUseProxies(false) (ES5 only)
@mweststrate
Copy link
Collaborator

Usually the quickest way to find these things out, is to disable that piece of code and see if a test breaks. Not sure if I understood your question correctly, but yes, I think it is indeed for "wrapping" cases like: draft.x = { y: draft.y }, which creates a plain object wrapping drafts. That is more common than you'd might expect since draft.todos = draft.todos.filter(x => x.done) does basically already create a plain object (well, array) wrapper around drafts.

@zthxxx
Copy link
Author

zthxxx commented Jan 11, 2023

@mweststrate This explanation is make scene to me, then I think ANOTHER reason in deep is the 93 line in each function,

it access obj[key] directly so that some getter will be called, but it's unnecessary in most of usage case call each function like "freeze property", on the other hand there are still a few cases need copy getter method or get value.

export function each(obj: any, iter: any, enumerableOnly = false) {
if (getArchtype(obj) === Archtype.Object) {
;(enumerableOnly ? Object.keys : ownKeys)(obj).forEach(key => {
if (!enumerableOnly || typeof key !== "symbol") iter(key, obj[key], obj)
})
} else {
obj.forEach((entry: any, index: any) => iter(index, entry, obj))
}
}

In any case, immer shouldn't even have to try freeze a getter, and for performance, while immer is copy-on-write, so I think the two cases in your explanation,

draft.x = { y: draft.y }
draft.todos = draft.todos.filter(x => x.done)

it shoud write draft.x and draft.todos to proxy draft object rather than plain object/array, means the proxy object of { y: draft.y } or array of draft.todos.filter(x => x.done) should created and in draft[Symbol.for("immer-state")].drafts_,

the process deal in "write" step of produce's recipe instead of in "return" step with ending of produce.

@mweststrate
Copy link
Collaborator

I'm not sure there is an easy non-expensive way of skipping over calling the getters, but feel to give it a try. In any case, having (non-idempotent) side effects usually creates problems in any case, even without Immer.

@zthxxx
Copy link
Author

zthxxx commented Jan 11, 2023

Actually, I have a series of array with each item that has a getter property to make lazy evaluations when they need to be used for the performance reasons, but immer called all of them even only make irrelevant modifications in produce(), so our page is stuck.

It's not really lacking side effects, but just have relatively more computationally intensive.

@mweststrate
Copy link
Collaborator

mweststrate commented Jan 11, 2023 via email

@mweststrate
Copy link
Collaborator

Immer 10 changed the approached to getters, so closing here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants