-
-
Notifications
You must be signed in to change notification settings - Fork 265
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
[v2] breaking: compatibility with memo #866
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this not "ideal" because every used key -- perhaps stale, from previous enders -- will now be remembered?
Whereas before,
affected
was re-created every render, ensuring the absence of possibly stale/unused keys, and therefore less checking inisChanged
.Why does React compiler require this exactly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @faceyspacey !
Exactly. You understand it perfectly.
This is not only the issue with the compiler. It has been an issue with React.memo and useMemo.
In Valtio, we has been recommending to avoid using React.memo and useMemo.
In React-Tracked, we had provided custom
memo
to replace React.memo (AFAIR, it's you who suggested it.)The real issue is, in the case of useMemo, it doesn't evaluate the function in every render.
Here's a simple repro:
Valtio v1 doesn't work well in this repro:
On the initial render,
.value
is touched.But, on the second render triggered by a parent component (not by useSnapshot), all tracking information will be gone. Same can happen with React compiler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's crazy that I was the one who suggested -- brings things full circle!
Truthfully I have a lot to share with you. I've been using Valtio now for 2 years, and React Tracked for the years before it. I'm almost done with something I've been working on for at least 6 years. I forked Valtio a year ago and started to add my own customizations. Just recently I did experiments to make it lazy -- I was the one who asked about this about 8 months ago from a company account, Skins App, for which I'm CTO. I've had success making the valtio "set" proxy lazy based on usage in the proxy-compare "get" proxy. But for now, I actually shifted to making it lazy based on an additional "get" proxy in the valtio "set" proxy. This has the benefit of being able to receive large nested data sets, which don't immediately become proxies. For example, say we receive and set this data:
Here's a simplified version of my
get
trap in the valtio proxy:Here's the no longer eager
set
trap:So going back to the example, essentially only the initial
users
array becomes a proxy, but the nested data won't until you access it, which interestingly is what will happen if you try to set it deeply, eg:proxy.users[0].nested.deeper.soOn.foo = 'bar'
.So it functions identically to your original
set
proxy, but only accidentally as a by-product of reaching deep into the nested data set.This allows you to have large and deeply nested objects that operate as your
refs
currently do. Note: I've removed the refs feature (and a few other things I don't need) to squeeze out a bit more performance. Essentially by using theget
trap we achieve automatic refs for anything that you never change. Keep in mind I have my entire app in valtio -- we have a lot of data in there!So this approach isn't as lazy as if I did it in the proxy-compare snapshot proxy, because proxies will still be setup for root objects when they are assigned, and because your app may set deeply nested things which snapshot proxies don't actually use. But I did make it work work in the proxy-compare snapshot proxy too, in case you were interested. My end goal is to revert back to that approach after I remove my own unrelated customizations in the valtio "get" proxy, which necessitate doing this work there anyway.
So all that said, I'd just like to confirm with you: the new memo handling is only to support passing objects to memoized children or an entire
snap
as in your above example, correct? This is as opposed to primitives.I've been following the rule of just passing primitives to memoized components, and have had zero problems. But I'm thinking the potential problem is that "I'm too good at using valtio", whereas new users may pass the entire object. The tradeoff you made in your latest V2 changes is in having to educate users about this, versus it just working [in all cases] as you have now, correct?
If so, that's a tough decision, because the tradeoff is we lose some performance in this approach. I personally wouldn't want to see this in my app. I get that there's now new React Compiler benefits. Frankly that won't be a big deal for my underlying tools because essentially all event handlers are statically setup once, and then maintain references. I actually have it so every component is memoized automatically by a babel plugin! I have some very novel discoveries about how to build apps very differently. And one of the benefits is you rarely use tools like
useCallback
nor have to fiddle with references to optimize rendering. Because you aren't creating event handler functions in components, you get automatic perf + DX. I have aPressable
component which is memoized to receive events and a nestedarg
prop.Therefore, given I don't worry about references and memoizations, React Compiler is probably more of a problem than a solution (since it is struggling to be perfect). From the perspective of Respond Framework managing references is considered "implementation details" (which we don't have to deal with because they are automatically handled!)
React Compiler may be incrementally better, but it's another thing from the react team which I don't use. I haven't used anything new since hooks. No transitions, no RSCs, no suspense. None of this. I've tried it all -- so I'm well versed in it -- but the way Respond Framework solves problems is completely different (and simplified!) and doesn't necessitate these things. From the perspective of Respond everything since hooks is "solutions to problems they have created."
Anyway, my last question relates to the memoization challenges -- and that's this line in
isChanged
:I recently purchased and watched your video on the simplified version of Valtio, which was great by the way! And if I'm correct, you are returning
true
also as a way to ensure memo still works when there is no record of what was accessed?And I'm guessing you no longer need to fallback to returning
true
with the v2 approach:correct?
I ask this because I'm optimizing for a more narrow use case, and I'd like to achieve the best possible perf. It seems in valtio v1 for apps that never pass
snap
objects asprops
to memoized components, you don't need this line either, correct?If correct, we can remove it and avoid a few possible unnecessary renders.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#836
I think I might have misunderstood one thing.
If we can make
set
lazy only with invanilla.ts
, it can be something considerable. But, I'm not sure the performance benefit or tradeoffs.Yes. You can easily confirm it with small examples.
That is correct, but the education is not the motivation of v2 changes. The motivation is to be compatible with upcoming React 19+.
Yeah, that's correct. But, otherwise, we would simply lose users because Valtio v1 can't be used with React compiler. And, it's probably easy to recover the lost performance with the compiler.
hm, tbh, I haven't thought about it.
I think we still require that. One thing is "proxy-compare" is used elsewhere too. And the change in valtio v2 doesn't ensure anything.
Another thing is that I think you misunderstood something.
While it's related to memo, the rationale is if
snap.obj
is accessed, butsnap.obj.foo
isn't,.obj
is marked as "used". Likewise, ifsnap.bar
isn't accessed,snap
itself is marked as "used".Hope it explains.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the other use case besides supporting memoized components (in valtio v1) is supporting
!!snab.obj
in order to conditionally render something. I fully understand now why we needif (changed === null) changed = true
.Regarding React Compiler, it seems it's just the same issue as before regarding memoized components that you passed snap objects to. The only difference is now users might be unaware that their child component is going to be memoized, causing more potential for users accidentally losing reactivity and therefore frustration.
Is there something else about React Compiler that causes valtio to breakdown? Perhaps something more explicit that crashes the app?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's a great example. Noted.
correct. not only child components, but also the component with
useSnapshot
.I don't believe there's any. But, we don't test it throughly. I'm waiting for user feedback.