-
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
Fix additional re-render when calling fetchMore
from useSuspenseQuery
when using startTransition
.
#11638
Fix additional re-render when calling fetchMore
from useSuspenseQuery
when using startTransition
.
#11638
Changes from 8 commits
a9fd458
b54c1d9
e9b52e3
97168f4
65bd9fe
6902673
8aa4ff2
8ef9d96
e5a012c
d9e2257
0fd1712
97ddb52
002eafd
7d07c58
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 | ||
--- | ||
|
||
Fix issue where calling `fetchMore` inside a `startTransition` from `useSuspenseQuery` causes an additional rerender. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
{ | ||
"dist/apollo-client.min.cjs": 39075, | ||
"dist/apollo-client.min.cjs": 39077, | ||
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32584 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import React, { Fragment, StrictMode, Suspense } from "react"; | ||
import React, { Fragment, StrictMode, Suspense, useTransition } from "react"; | ||
import { | ||
act, | ||
screen, | ||
|
@@ -51,7 +51,15 @@ import { | |
RefetchWritePolicy, | ||
WatchQueryFetchPolicy, | ||
} from "../../../core/watchQueryOptions"; | ||
import { profile, spyOnConsole } from "../../../testing/internal"; | ||
import { | ||
PaginatedCaseData, | ||
PaginatedCaseVariables, | ||
createProfiler, | ||
profile, | ||
setupPaginatedCase, | ||
spyOnConsole, | ||
useTrackRenders, | ||
} from "../../../testing/internal"; | ||
|
||
type RenderSuspenseHookOptions<Props, TSerializedCache = {}> = Omit< | ||
RenderHookOptions<Props>, | ||
|
@@ -9978,6 +9986,128 @@ describe("useSuspenseQuery", () => { | |
}); | ||
}); | ||
|
||
// https://github.com/apollographql/apollo-client/issues/11315 | ||
it("fetchMore does not cause extra render", async () => { | ||
const { query, link } = setupPaginatedCase(); | ||
|
||
const user = userEvent.setup(); | ||
const client = new ApolloClient({ | ||
cache: new InMemoryCache({ | ||
typePolicies: { | ||
Query: { | ||
fields: { | ||
letters: offsetLimitPagination(), | ||
}, | ||
}, | ||
}, | ||
}), | ||
link, | ||
}); | ||
|
||
const Profiler = createProfiler({ | ||
initialSnapshot: { | ||
result: null as UseSuspenseQueryResult< | ||
PaginatedCaseData, | ||
PaginatedCaseVariables | ||
> | null, | ||
}, | ||
}); | ||
|
||
function SuspenseFallback() { | ||
useTrackRenders(); | ||
|
||
return <div>Loading...</div>; | ||
} | ||
|
||
function App() { | ||
useTrackRenders(); | ||
const [isPending, startTransition] = useTransition(); | ||
const result = useSuspenseQuery(query, { | ||
variables: { offset: 0, limit: 2 }, | ||
}); | ||
const { data, fetchMore } = result; | ||
|
||
Profiler.mergeSnapshot({ result }); | ||
|
||
return ( | ||
<button | ||
disabled={isPending} | ||
onClick={() => | ||
startTransition(() => { | ||
fetchMore({ | ||
variables: { | ||
offset: data.letters.length, | ||
limit: data.letters.length + 1, | ||
}, | ||
}); | ||
}) | ||
} | ||
> | ||
Fetch next | ||
</button> | ||
); | ||
} | ||
|
||
render(<App />, { | ||
wrapper: ({ children }) => ( | ||
<ApolloProvider client={client}> | ||
<Profiler> | ||
<Suspense fallback={<SuspenseFallback />}>{children}</Suspense> | ||
</Profiler> | ||
</ApolloProvider> | ||
), | ||
}); | ||
|
||
{ | ||
const { renderedComponents } = await Profiler.takeRender(); | ||
|
||
expect(renderedComponents).toStrictEqual([SuspenseFallback]); | ||
} | ||
|
||
{ | ||
const { snapshot, renderedComponents } = await Profiler.takeRender(); | ||
|
||
expect(renderedComponents).toStrictEqual([App]); | ||
expect(snapshot.result?.data).toEqual({ | ||
letters: [ | ||
{ __typename: "Letter", letter: "A", position: 1 }, | ||
{ __typename: "Letter", letter: "B", position: 2 }, | ||
], | ||
}); | ||
} | ||
|
||
await act(() => user.click(screen.getByText("Fetch next"))); | ||
|
||
{ | ||
const { snapshot, renderedComponents } = await Profiler.takeRender(); | ||
|
||
expect(renderedComponents).toStrictEqual([App]); | ||
expect(snapshot.result?.data).toEqual({ | ||
letters: [ | ||
{ __typename: "Letter", letter: "A", position: 1 }, | ||
{ __typename: "Letter", letter: "B", position: 2 }, | ||
], | ||
}); | ||
} | ||
|
||
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. Reading the test, it's not immediate apparent why this rerender happens. I'd assume to disable the button - but it might be good in that case to do an 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. Good call! d9e2257 |
||
{ | ||
const { snapshot, renderedComponents } = await Profiler.takeRender(); | ||
|
||
expect(renderedComponents).toStrictEqual([App]); | ||
expect(snapshot.result?.data).toEqual({ | ||
letters: [ | ||
{ __typename: "Letter", letter: "A", position: 1 }, | ||
{ __typename: "Letter", letter: "B", position: 2 }, | ||
{ __typename: "Letter", letter: "C", position: 3 }, | ||
{ __typename: "Letter", letter: "D", position: 4 }, | ||
{ __typename: "Letter", letter: "E", position: 5 }, | ||
], | ||
}); | ||
} | ||
|
||
await expect(Profiler).not.toRerender(); | ||
}); | ||
|
||
describe.skip("type tests", () => { | ||
it("returns unknown when TData cannot be inferred", () => { | ||
const query = gql` | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -80,8 +80,8 @@ export interface PaginatedCaseVariables { | |
export function setupPaginatedCase() { | ||
const query: TypedDocumentNode<PaginatedCaseData, PaginatedCaseVariables> = | ||
gql` | ||
query letters($limit: Int, $offset: Int) { | ||
letters(limit: $limit) { | ||
query LettersQuery($limit: Int, $offset: Int) { | ||
letters(limit: $limit, offset: $offset) { | ||
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 offset as a variable is important for cache reasons and it was forgotten 😱. Thankfully no tests were harmed in this change, but it was necessary to reproduce the bug in this PR. |
||
letter | ||
position | ||
} | ||
|
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.
Narrator: It was related
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.
Made me laugh. Also, great we're rid of them now!