-
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
Changes from all commits
4566b34
6652d94
f1ea3a1
180fe81
6a62bed
c08a56c
30aae50
5e1e8a2
c0b6166
6c1372c
f51d7ad
087fc4c
ecf9427
0d88921
a10fc98
58384c3
524aae7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@apollo/client': patch | ||
--- | ||
|
||
Prevent `useFragment` from excessively unsubscribing and resubscribing the fragment with the cache on every render. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
{ | ||
"dist/apollo-client.min.cjs": 37901, | ||
"dist/apollo-client.min.cjs": 37932, | ||
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 31976 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
// These hooks are used internally and are not exported publicly by the library | ||
export { useDeepMemo } from "./useDeepMemo.js"; | ||
export { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js"; | ||
export { useLazyRef } from "./useLazyRef.js"; | ||
export { __use } from "./__use.js"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import * as React from "react"; | ||
|
||
const INIT = {}; | ||
|
||
export function useLazyRef<T>(getInitialValue: () => T) { | ||
const ref = React.useRef<T>(INIT as unknown as T); | ||
|
||
if (ref.current === INIT) { | ||
ref.current = getInitialValue(); | ||
} | ||
|
||
return ref; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ import { useApolloClient } from "./useApolloClient.js"; | |
import { useSyncExternalStore } from "./useSyncExternalStore.js"; | ||
import type { OperationVariables } from "../../core/index.js"; | ||
import type { NoInfer } from "../types/types.js"; | ||
import { useDeepMemo, useLazyRef } from "./internal/index.js"; | ||
|
||
export interface UseFragmentOptions<TData, TVars> | ||
extends Omit< | ||
|
@@ -46,48 +47,57 @@ export function useFragment<TData = any, TVars = OperationVariables>( | |
): UseFragmentResult<TData> { | ||
const { cache } = useApolloClient(); | ||
|
||
const { fragment, fragmentName, from, optimistic = true, ...rest } = options; | ||
const diffOptions = useDeepMemo<Cache.DiffOptions<TData, TVars>>(() => { | ||
const { | ||
fragment, | ||
fragmentName, | ||
from, | ||
optimistic = true, | ||
...rest | ||
} = options; | ||
|
||
const diffOptions: Cache.DiffOptions<TData, TVars> = { | ||
...rest, | ||
returnPartialData: true, | ||
id: typeof from === "string" ? from : cache.identify(from), | ||
query: cache["getFragmentDoc"](fragment, fragmentName), | ||
optimistic, | ||
}; | ||
return { | ||
...rest, | ||
returnPartialData: true, | ||
id: typeof from === "string" ? from : cache.identify(from), | ||
query: cache["getFragmentDoc"](fragment, fragmentName), | ||
optimistic, | ||
}; | ||
}, [options]); | ||
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more. Introducing this There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 |
||
diffToResult(cache.diff<TData>(diffOptions)) | ||
); | ||
|
||
// Used for both getSnapshot and getServerSnapshot | ||
const getSnapshot = () => { | ||
const latestDiffToResult = diffToResult(latestDiff); | ||
return ( | ||
resultRef.current && | ||
equal(resultRef.current.data, latestDiffToResult.data) | ||
) ? | ||
resultRef.current | ||
: (resultRef.current = latestDiffToResult); | ||
}; | ||
const getSnapshot = React.useCallback(() => resultRef.current, []); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This prevents React from doing extra work when the |
||
|
||
return useSyncExternalStore( | ||
(forceUpdate) => { | ||
let lastTimeout = 0; | ||
const unsubcribe = cache.watch({ | ||
...diffOptions, | ||
immediate: true, | ||
callback(diff) { | ||
if (!equal(diff, latestDiff)) { | ||
resultRef.current = diffToResult((latestDiff = diff)); | ||
lastTimeout = setTimeout(forceUpdate) as any; | ||
} | ||
}, | ||
}); | ||
return () => { | ||
unsubcribe(); | ||
clearTimeout(lastTimeout); | ||
}; | ||
}, | ||
React.useCallback( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Turns out we had tried to re-render a 2nd time in There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
(forceUpdate) => { | ||
let lastTimeout = 0; | ||
const unsubscribe = cache.watch({ | ||
...diffOptions, | ||
immediate: true, | ||
callback(diff) { | ||
if (!equal(diff.result, resultRef.current.data)) { | ||
resultRef.current = diffToResult(diff); | ||
// If we get another update before we've re-rendered, bail out of | ||
// the update and try again. This ensures that the relative timing | ||
// between useQuery and useFragment stays roughly the same as | ||
// fixed in https://github.com/apollographql/apollo-client/pull/11083 | ||
clearTimeout(lastTimeout); | ||
lastTimeout = setTimeout(forceUpdate) as any; | ||
} | ||
}, | ||
}); | ||
return () => { | ||
unsubscribe(); | ||
clearTimeout(lastTimeout); | ||
}; | ||
}, | ||
[cache, diffOptions] | ||
), | ||
getSnapshot, | ||
getSnapshot | ||
); | ||
|
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 likeundefined
makes sure that we can still setundefined
as a value on subsequent renders without re-running the init function.