diff --git a/.changeset/flat-singers-kiss.md b/.changeset/flat-singers-kiss.md new file mode 100644 index 00000000000..74727e85370 --- /dev/null +++ b/.changeset/flat-singers-kiss.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Fix issue where passing a new `from` option to `useFragment` would first render with the previous value before rerendering with the correct value. diff --git a/.size-limits.json b/.size-limits.json index fc5f57b1041..6ea6a4c6ebc 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39248, + "dist/apollo-client.min.cjs": 39267, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32630 } diff --git a/src/react/hooks/__tests__/useFragment.test.tsx b/src/react/hooks/__tests__/useFragment.test.tsx index 27f4857edd6..d5436bc5097 100644 --- a/src/react/hooks/__tests__/useFragment.test.tsx +++ b/src/react/hooks/__tests__/useFragment.test.tsx @@ -29,7 +29,7 @@ import { concatPagination } from "../../../utilities"; import assert from "assert"; import { expectTypeOf } from "expect-type"; import { SubscriptionObserver } from "zen-observable-ts"; -import { profile, spyOnConsole } from "../../../testing/internal"; +import { profile, profileHook, spyOnConsole } from "../../../testing/internal"; describe("useFragment", () => { it("is importable and callable", () => { @@ -1359,6 +1359,61 @@ describe("useFragment", () => { }); }); + it("returns correct data when options change", async () => { + const client = new ApolloClient({ + cache: new InMemoryCache(), + }); + type User = { __typename: "User"; id: number; name: string }; + const fragment: TypedDocumentNode = gql` + fragment UserFragment on User { + id + name + } + `; + + client.writeFragment({ + fragment, + data: { __typename: "User", id: 1, name: "Alice" }, + }); + + client.writeFragment({ + fragment, + data: { __typename: "User", id: 2, name: "Charlie" }, + }); + + const ProfiledHook = profileHook(({ id }: { id: number }) => + useFragment({ fragment, from: { __typename: "User", id } }) + ); + + const { rerender } = render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + { + const snapshot = await ProfiledHook.takeSnapshot(); + + expect(snapshot).toEqual({ + complete: true, + data: { __typename: "User", id: 1, name: "Alice" }, + }); + } + + rerender(); + + { + const snapshot = await ProfiledHook.takeSnapshot(); + + expect(snapshot).toEqual({ + complete: true, + data: { __typename: "User", id: 2, name: "Charlie" }, + }); + } + + await expect(ProfiledHook).not.toRerender(); + }); + describe("tests with incomplete data", () => { let cache: InMemoryCache, wrapper: React.FunctionComponent; const ItemFragment = gql` diff --git a/src/react/hooks/useFragment.ts b/src/react/hooks/useFragment.ts index 9e5381c7480..368e6273d85 100644 --- a/src/react/hooks/useFragment.ts +++ b/src/react/hooks/useFragment.ts @@ -88,6 +88,12 @@ function _useFragment( diffToResult(cache.diff(diffOptions)) ); + // 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(diffOptions)); + }, [diffOptions, cache]); + // Used for both getSnapshot and getServerSnapshot const getSnapshot = React.useCallback(() => resultRef.current, []);