-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Avoid excessive cache.watch
/ unsubscribe on every render in useFragment
#11464
Conversation
🦋 Changeset detectedLatest commit: 524aae7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
size-limit report 📦
|
clearTimeout(lastTimeout); | ||
}; | ||
}, | ||
React.useCallback( |
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.
Oddly, this change seems to now throw off the timing of the update between useQuery
and useFragment
when used in the same component.
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.
Turns out we had tried to re-render a 2nd time in useFragment
before the first had finished, so a lingering setTimeout
had been queued before our cache.writeQuery
in the test. Because of this, useFragment
would re-render before useQuery
and threw of the timing. I was able to fix this in 6c1372c by cancelling the timeout from the previous attempt in case it hadn't yet flushed before queuing another attempt at the render.
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.
Turns out this change also makes the timing in React 17 and 18 the same. I was able to remove the additional React version check in the test.
cache.watch
/ unsubscribe on every render in useFragment
cache.watch
/ unsubscribe on every render in useFragment
const INIT = {}; | ||
|
||
export function useLazyRef<T>(getInitialValue: () => T) { | ||
const ref = React.useRef<T>(INIT as unknown as T); |
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.
Comparing against this INIT
sentinel here instead of something like undefined
makes sure that we can still set undefined
as a value on subsequent renders without re-running the init function.
resultRef.current | ||
: (resultRef.current = latestDiffToResult); | ||
}; | ||
const getSnapshot = React.useCallback(() => resultRef.current, []); |
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.
This prevents React from doing extra work when the getSnapshot
function isn't stable across renders.
|
||
const resultRef = React.useRef<UseFragmentResult<TData>>(); | ||
let latestDiff = cache.diff<TData>(diffOptions); | ||
const resultRef = useLazyRef<UseFragmentResult<TData>>(() => |
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.
Introducing this useLazyRef
avoids the need to call cache.diff
on every render which should give a small perf benefit.
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.
Did you check if we can utilize this in other places in our codebase?
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 just did a quick glance through some of our other hooks. There is only one other place right now that could potentially utilize it, which is the useInternalState
hook in useQuery
that initializes the InternalState
instance (would still need some of that if
statement though, so not sure what the bundle size diff would be)
clearTimeout(lastTimeout); | ||
}; | ||
}, | ||
React.useCallback( |
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.
Turns out this change also makes the timing in React 17 and 18 the same. I was able to remove the additional React version check in the test.
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.
Looks very good - thank you for digging into this! :)
|
||
const resultRef = React.useRef<UseFragmentResult<TData>>(); | ||
let latestDiff = cache.diff<TData>(diffOptions); | ||
const resultRef = useLazyRef<UseFragmentResult<TData>>(() => |
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.
Did you check if we can utilize this in other places in our codebase?
While looking through
useFragment
, I happened to notice that the subscribe callback passed touseSyncExternalStore
was not wrapped in auseCallback
. Because of this,useFragment
will unsubscribe from the previouscache.watch
and resubscribe with acache.watch
on every render. Whilecache.watch
should be fairly low overhead, there is no need to have to unsubscribe/resubscribe on every render and instead thecache.watch
should last until either the cache has been changed, or the options have changed.Checklist: