-
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
Remove double initialization and unneeded useLazyRef from useFragment to avoid write to ref in render #12020
Conversation
🦋 Changeset detectedLatest commit: 49dcf2e 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 📦
|
✅ Deploy Preview for apollo-client-docs ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
src/react/hooks/useFragment.ts
Outdated
// Since .next is async, we need to make sure that we | ||
// get the correct diff on the next render given new diffOptions | ||
React.useMemo(() => { | ||
const { | ||
fragment, | ||
fragmentName, | ||
from, | ||
optimistic = true, | ||
...rest | ||
} = options; | ||
|
||
return { | ||
...rest, | ||
returnPartialData: true, | ||
id: typeof from === "string" ? from : cache.identify(from), | ||
query: cache["getFragmentDoc"](fragment, fragmentName), | ||
optimistic, | ||
}; | ||
}, [options]); | ||
} = stableOptions; | ||
|
||
const resultRef = useLazyRef<UseFragmentResult<TData>>(() => | ||
diffToResult(cache.diff<TData>(diffOptions)) | ||
); | ||
|
||
const stableOptions = useDeepMemo(() => options, [options]); | ||
|
||
// Since .next is async, we need to make sure that we | ||
// get the correct diff on the next render given new diffOptions | ||
React.useMemo(() => { | ||
resultRef.current = diffToResult(cache.diff<TData>(diffOptions)); | ||
}, [diffOptions, cache]); | ||
resultRef.current = diffToResult( | ||
cache.diff<TData>({ | ||
...rest, | ||
returnPartialData: true, | ||
id: typeof from === "string" ? from : cache.identify(from), | ||
query: cache["getFragmentDoc"](fragment, fragmentName), | ||
optimistic, | ||
}) | ||
); | ||
}, [stableOptions, cache]); | ||
|
||
// Used for both getSnapshot and getServerSnapshot | ||
const getSnapshot = React.useCallback(() => resultRef.current, []); | ||
const getSnapshot = React.useCallback(() => resultRef.current!, []); | ||
|
||
return useSyncExternalStore( | ||
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.
What do you think about this?
// Since .next is async, we need to make sure that we | |
// get the correct diff on the next render given new diffOptions | |
React.useMemo(() => { | |
const { | |
fragment, | |
fragmentName, | |
from, | |
optimistic = true, | |
...rest | |
} = options; | |
return { | |
...rest, | |
returnPartialData: true, | |
id: typeof from === "string" ? from : cache.identify(from), | |
query: cache["getFragmentDoc"](fragment, fragmentName), | |
optimistic, | |
}; | |
}, [options]); | |
} = stableOptions; | |
const resultRef = useLazyRef<UseFragmentResult<TData>>(() => | |
diffToResult(cache.diff<TData>(diffOptions)) | |
); | |
const stableOptions = useDeepMemo(() => options, [options]); | |
// Since .next is async, we need to make sure that we | |
// get the correct diff on the next render given new diffOptions | |
React.useMemo(() => { | |
resultRef.current = diffToResult(cache.diff<TData>(diffOptions)); | |
}, [diffOptions, cache]); | |
resultRef.current = diffToResult( | |
cache.diff<TData>({ | |
...rest, | |
returnPartialData: true, | |
id: typeof from === "string" ? from : cache.identify(from), | |
query: cache["getFragmentDoc"](fragment, fragmentName), | |
optimistic, | |
}) | |
); | |
}, [stableOptions, cache]); | |
// Used for both getSnapshot and getServerSnapshot | |
const getSnapshot = React.useCallback(() => resultRef.current, []); | |
const getSnapshot = React.useCallback(() => resultRef.current!, []); | |
return useSyncExternalStore( | |
React.useCallback( | |
// Since .next is async, we need to make sure that we | |
// get the correct diff on the next render given new diffOptions | |
const currentDiff = React.useMemo(() => { | |
const { | |
fragment, | |
fragmentName, | |
from, | |
optimistic = true, | |
...rest | |
} = stableOptions; | |
return diffToResult( | |
cache.diff<TData>({ | |
...rest, | |
returnPartialData: true, | |
id: typeof from === "string" ? from : cache.identify(from), | |
query: cache["getFragmentDoc"](fragment, fragmentName), | |
optimistic, | |
}) | |
); | |
}, [stableOptions, cache]); | |
// Used for both getSnapshot and getServerSnapshot | |
const getSnapshot = React.useCallback( | |
() => resultRef.current || currentDiff, | |
[currentDiff] | |
); | |
return useSyncExternalStore( | |
React.useCallback( |
and then in the cleanup
return () => {
resultRef.current = undefined;
subscription.unsubscribe();
clearTimeout(lastTimeout);
};
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 like that causes some tests failures. I'll dig in a bit more after planning to try and understand why this change causes it because I like this change a lot.
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.
Ok seems this fails so many tests because the check to determine if the current result matches the last one fails on the initial render:
if (equal(result, resultRef.current)) return;
With this change in place, resultRef.current
is undefined
, so this check will always fail, therefore we get an extra render since the hook thinks the result is different.
This check is really only used for the first result emitted from watchFragment
, specifically for this case of initialization since watchFragment
emits its initial value asynchronously and we want to avoid an extra render for the initial value.
Let me see if comparing against currentResult
in uSES (which means adding it to the dependency array) causes any failures here.
3023b71
to
673245d
Compare
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 looks good to me now :)
While working on data masking with fragments, I stumbled upon a small optimization that we can make in
useFragment
. This hook usescache.diff
directly in order to get an initial value returned from the first execution of the hook. This initial value was set usinguseLazyRef
. We also had a bit of code within auseMemo
that also set the ref's value to handle when options change to ensure we were able to get a synchronous value that way. ThisuseMemo
means we don't actually need thatuseLazyRef
since theuseMemo
will also initialize the ref value if its not already set.By replacing
useLazyRef
withuseRef
, we were able to remove theuseLazyRef
hook entirely and inline the creation of diff options in the sameuseMemo
call. This reduces a bit of bundle size as a result.