Skip to content

Commit 82d8cb4

Browse files
authored
Remove double initialization and unneeded useLazyRef from useFragment to avoid write to ref in render (#12020)
1 parent eb3e21b commit 82d8cb4

File tree

5 files changed

+41
-43
lines changed

5 files changed

+41
-43
lines changed

Diff for: .changeset/tough-geese-sing.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client": patch
3+
---
4+
5+
Better conform to Rules of React by avoiding write of ref in render for `useFragment`.

Diff for: .size-limits.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"dist/apollo-client.min.cjs": 40271,
2+
"dist/apollo-client.min.cjs": 40252,
33
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 33059
44
}

Diff for: src/react/hooks/internal/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,5 @@
22
export { useDeepMemo } from "./useDeepMemo.js";
33
export { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js";
44
export { useRenderGuard } from "./useRenderGuard.js";
5-
export { useLazyRef } from "./useLazyRef.js";
65
export { __use } from "./__use.js";
76
export { wrapHook } from "./wrapHook.js";

Diff for: src/react/hooks/internal/useLazyRef.ts

-13
This file was deleted.

Diff for: src/react/hooks/useFragment.ts

+35-28
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { useApolloClient } from "./useApolloClient.js";
1212
import { useSyncExternalStore } from "./useSyncExternalStore.js";
1313
import type { ApolloClient, OperationVariables } from "../../core/index.js";
1414
import type { NoInfer } from "../types/types.js";
15-
import { useDeepMemo, useLazyRef, wrapHook } from "./internal/index.js";
15+
import { useDeepMemo, wrapHook } from "./internal/index.js";
1616
import equal from "@wry/equality";
1717

1818
export interface UseFragmentOptions<TData, TVars>
@@ -64,47 +64,53 @@ function _useFragment<TData = any, TVars = OperationVariables>(
6464
options: UseFragmentOptions<TData, TVars>
6565
): UseFragmentResult<TData> {
6666
const { cache } = useApolloClient(options.client);
67+
const { from, ...rest } = options;
6768

68-
const diffOptions = useDeepMemo<Cache.DiffOptions<TData, TVars>>(() => {
69-
const {
70-
fragment,
71-
fragmentName,
72-
from,
73-
optimistic = true,
74-
...rest
75-
} = options;
76-
77-
return {
78-
...rest,
79-
returnPartialData: true,
80-
id: typeof from === "string" ? from : cache.identify(from),
81-
query: cache["getFragmentDoc"](fragment, fragmentName),
82-
optimistic,
83-
};
84-
}, [options]);
85-
86-
const resultRef = useLazyRef<UseFragmentResult<TData>>(() =>
87-
diffToResult(cache.diff<TData>(diffOptions))
69+
// We calculate the cache id seperately from `stableOptions` because we don't
70+
// want changes to non key fields in the `from` property to affect
71+
// `stableOptions` and retrigger our subscription. If the cache identifier
72+
// stays the same between renders, we want to reuse the existing subscription.
73+
const id = React.useMemo(
74+
() => (typeof from === "string" ? from : cache.identify(from)),
75+
[cache, from]
8876
);
8977

90-
const stableOptions = useDeepMemo(() => options, [options]);
78+
const resultRef = React.useRef<UseFragmentResult<TData>>();
79+
const stableOptions = useDeepMemo(() => ({ ...rest, from: id! }), [rest, id]);
9180

9281
// Since .next is async, we need to make sure that we
9382
// get the correct diff on the next render given new diffOptions
94-
React.useMemo(() => {
95-
resultRef.current = diffToResult(cache.diff<TData>(diffOptions));
96-
}, [diffOptions, cache]);
83+
const currentDiff = React.useMemo(() => {
84+
const { fragment, fragmentName, from, optimistic = true } = stableOptions;
85+
86+
return diffToResult(
87+
cache.diff<TData>({
88+
...stableOptions,
89+
returnPartialData: true,
90+
id: from,
91+
query: cache["getFragmentDoc"](fragment, fragmentName),
92+
optimistic,
93+
})
94+
);
95+
}, [stableOptions, cache]);
9796

9897
// Used for both getSnapshot and getServerSnapshot
99-
const getSnapshot = React.useCallback(() => resultRef.current, []);
98+
const getSnapshot = React.useCallback(
99+
() => resultRef.current || currentDiff,
100+
[currentDiff]
101+
);
100102

101103
return useSyncExternalStore(
102104
React.useCallback(
103105
(forceUpdate) => {
104106
let lastTimeout = 0;
105107
const subscription = cache.watchFragment(stableOptions).subscribe({
106108
next: (result) => {
107-
if (equal(result, resultRef.current)) return;
109+
// Since `next` is called async by zen-observable, we want to avoid
110+
// unnecessarily rerendering this hook for the initial result
111+
// emitted from watchFragment which should be equal to
112+
// `currentDiff`.
113+
if (equal(result, currentDiff)) return;
108114
resultRef.current = result;
109115
// If we get another update before we've re-rendered, bail out of
110116
// the update and try again. This ensures that the relative timing
@@ -115,11 +121,12 @@ function _useFragment<TData = any, TVars = OperationVariables>(
115121
},
116122
});
117123
return () => {
124+
resultRef.current = void 0;
118125
subscription.unsubscribe();
119126
clearTimeout(lastTimeout);
120127
};
121128
},
122-
[cache, stableOptions]
129+
[cache, stableOptions, currentDiff]
123130
),
124131
getSnapshot,
125132
getSnapshot

0 commit comments

Comments
 (0)