Skip to content

Commit

Permalink
Remove retain call from useBackgroundQuery to allow for auto disp…
Browse files Browse the repository at this point in the history
…osal (#11438)
  • Loading branch information
jerelmiller authored Dec 19, 2023
1 parent 3cbb207 commit 6d46ab9
Show file tree
Hide file tree
Showing 4 changed files with 299 additions and 3 deletions.
7 changes: 7 additions & 0 deletions .changeset/wise-news-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@apollo/client': minor
---

Remove the need to call `retain` from `useBackgroundQuery` since `useReadQuery` will now retain the query. This means that a `queryRef` that is not consumed by `useReadQuery` within the given `autoDisposeTimeoutMs` will now be auto diposed for you.

Thanks to [#11412](https://github.com/apollographql/apollo-client/pull/11412), disposed query refs will be automatically resubscribed to the query when consumed by `useReadQuery` after it has been disposed.
2 changes: 1 addition & 1 deletion .size-limits.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"dist/apollo-client.min.cjs": 39135,
"dist/apollo-client.min.cjs": 39130,
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32651
}
291 changes: 291 additions & 0 deletions src/react/hooks/__tests__/useBackgroundQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
MockSubscriptionLink,
mockSingleLink,
MockedProvider,
wait,
} from "../../../testing";
import {
concatPagination,
Expand Down Expand Up @@ -54,6 +55,10 @@ import {
useTrackRenders,
} from "../../../testing/internal";

afterEach(() => {
jest.useRealTimers();
});

function createDefaultTrackedComponents<
Snapshot extends { result: UseReadQueryResult<any> | null },
TData = Snapshot["result"] extends UseReadQueryResult<infer TData> | null ?
Expand Down Expand Up @@ -155,6 +160,292 @@ it("fetches a simple query with minimal config", async () => {
await expect(Profiler).not.toRerender({ timeout: 50 });
});

it("tears down the query on unmount", async () => {
const { query, mocks } = setupSimpleCase();
const client = new ApolloClient({
link: new MockLink(mocks),
cache: new InMemoryCache(),
});
const Profiler = createDefaultProfiler<SimpleCaseData>();
const { SuspenseFallback, ReadQueryHook } =
createDefaultTrackedComponents(Profiler);

function App() {
useTrackRenders();
const [queryRef] = useBackgroundQuery(query);

return (
<Suspense fallback={<SuspenseFallback />}>
<ReadQueryHook queryRef={queryRef} />
</Suspense>
);
}

const { unmount } = renderWithClient(<App />, { client, wrapper: Profiler });

// initial suspended render
await Profiler.takeRender();

{
const { snapshot } = await Profiler.takeRender();

expect(snapshot.result).toEqual({
data: { greeting: "Hello" },
error: undefined,
networkStatus: NetworkStatus.ready,
});
}

unmount();

await wait(0);

expect(client.getObservableQueries().size).toBe(0);
expect(client).not.toHaveSuspenseCacheEntryUsing(query);
});

it("auto disposes of the queryRef if not used within timeout", async () => {
jest.useFakeTimers();
const { query } = setupSimpleCase();
const link = new MockSubscriptionLink();
const client = new ApolloClient({ link, cache: new InMemoryCache() });

const { result } = renderHook(() => useBackgroundQuery(query, { client }));

const [queryRef] = result.current;

expect(queryRef).not.toBeDisposed();
expect(client.getObservableQueries().size).toBe(1);
expect(client).toHaveSuspenseCacheEntryUsing(query);

await act(() => {
link.simulateResult({ result: { data: { greeting: "Hello" } } }, true);
// Ensure simulateResult will deliver the result since its wrapped with
// setTimeout
jest.advanceTimersByTime(10);
});

jest.advanceTimersByTime(30_000);

expect(queryRef).toBeDisposed();
expect(client.getObservableQueries().size).toBe(0);
expect(client).not.toHaveSuspenseCacheEntryUsing(query);
});

it("auto disposes of the queryRef if not used within configured timeout", async () => {
jest.useFakeTimers();
const { query } = setupSimpleCase();
const link = new MockSubscriptionLink();
const client = new ApolloClient({
link,
cache: new InMemoryCache(),
defaultOptions: {
react: {
suspense: {
autoDisposeTimeoutMs: 5000,
},
},
},
});

const { result } = renderHook(() => useBackgroundQuery(query, { client }));

const [queryRef] = result.current;

expect(queryRef).not.toBeDisposed();
expect(client.getObservableQueries().size).toBe(1);
expect(client).toHaveSuspenseCacheEntryUsing(query);

await act(() => {
link.simulateResult({ result: { data: { greeting: "Hello" } } }, true);
// Ensure simulateResult will deliver the result since its wrapped with
// setTimeout
jest.advanceTimersByTime(10);
});

jest.advanceTimersByTime(5000);

expect(queryRef).toBeDisposed();
expect(client.getObservableQueries().size).toBe(0);
expect(client).not.toHaveSuspenseCacheEntryUsing(query);
});

it("will resubscribe after disposed when mounting useReadQuery", async () => {
const { query, mocks } = setupSimpleCase();
const user = userEvent.setup();
const client = new ApolloClient({
link: new MockLink(mocks),
cache: new InMemoryCache(),
defaultOptions: {
react: {
suspense: {
// Set this to something really low to avoid fake timers
autoDisposeTimeoutMs: 20,
},
},
},
});

const Profiler = createDefaultProfiler<SimpleCaseData>();
const { SuspenseFallback, ReadQueryHook } =
createDefaultTrackedComponents(Profiler);

function App() {
useTrackRenders();
const [show, setShow] = React.useState(false);
const [queryRef] = useBackgroundQuery(query);

return (
<>
<button onClick={() => setShow((show) => !show)}>Toggle</button>
<Suspense fallback={<SuspenseFallback />}>
{show && <ReadQueryHook queryRef={queryRef} />}
</Suspense>
</>
);
}

renderWithClient(<App />, { client, wrapper: Profiler });

expect(client.getObservableQueries().size).toBe(1);
expect(client).toHaveSuspenseCacheEntryUsing(query);

{
const { renderedComponents } = await Profiler.takeRender();

expect(renderedComponents).toStrictEqual([App]);
}

// Wait long enough for auto dispose to kick in
await wait(50);

expect(client.getObservableQueries().size).toBe(0);
expect(client).not.toHaveSuspenseCacheEntryUsing(query);

await act(() => user.click(screen.getByText("Toggle")));

{
const { snapshot, renderedComponents } = await Profiler.takeRender();

expect(renderedComponents).toStrictEqual([App, ReadQueryHook]);
expect(snapshot.result).toEqual({
data: { greeting: "Hello" },
error: undefined,
networkStatus: NetworkStatus.ready,
});
}

client.writeQuery({
query,
data: { greeting: "Hello again" },
});

{
const { snapshot, renderedComponents } = await Profiler.takeRender();

expect(renderedComponents).toStrictEqual([ReadQueryHook]);
expect(snapshot.result).toEqual({
data: { greeting: "Hello again" },
error: undefined,
networkStatus: NetworkStatus.ready,
});
}

await expect(Profiler).not.toRerender({ timeout: 50 });
});

it("auto resubscribes when mounting useReadQuery after naturally disposed by useReadQuery", async () => {
const { query, mocks } = setupSimpleCase();
const user = userEvent.setup();
const client = new ApolloClient({
link: new MockLink(mocks),
cache: new InMemoryCache(),
});

const Profiler = createDefaultProfiler<SimpleCaseData>();
const { SuspenseFallback, ReadQueryHook } =
createDefaultTrackedComponents(Profiler);

function App() {
useTrackRenders();
const [show, setShow] = React.useState(true);
const [queryRef] = useBackgroundQuery(query);

return (
<>
<button onClick={() => setShow((show) => !show)}>Toggle</button>
<Suspense fallback={<SuspenseFallback />}>
{show && <ReadQueryHook queryRef={queryRef} />}
</Suspense>
</>
);
}

renderWithClient(<App />, { client, wrapper: Profiler });

const toggleButton = screen.getByText("Toggle");

expect(client.getObservableQueries().size).toBe(1);
expect(client).toHaveSuspenseCacheEntryUsing(query);

{
const { renderedComponents } = await Profiler.takeRender();

expect(renderedComponents).toStrictEqual([App, SuspenseFallback]);
}

{
const { snapshot } = await Profiler.takeRender();

expect(snapshot.result).toEqual({
data: { greeting: "Hello" },
error: undefined,
networkStatus: NetworkStatus.ready,
});
}

await act(() => user.click(toggleButton));
await Profiler.takeRender();
await wait(0);

expect(client.getObservableQueries().size).toBe(0);
expect(client).not.toHaveSuspenseCacheEntryUsing(query);

await act(() => user.click(toggleButton));

expect(client.getObservableQueries().size).toBe(1);
expect(client).toHaveSuspenseCacheEntryUsing(query);

{
const { snapshot, renderedComponents } = await Profiler.takeRender();

expect(renderedComponents).toStrictEqual([App, ReadQueryHook]);
expect(snapshot.result).toEqual({
data: { greeting: "Hello" },
error: undefined,
networkStatus: NetworkStatus.ready,
});
}

client.writeQuery({
query,
data: { greeting: "Hello again" },
});

{
const { snapshot, renderedComponents } = await Profiler.takeRender();

expect(renderedComponents).toStrictEqual([ReadQueryHook]);
expect(snapshot.result).toEqual({
data: { greeting: "Hello again" },
error: undefined,
networkStatus: NetworkStatus.ready,
});
}

await expect(Profiler).not.toRerender({ timeout: 50 });
});

it("allows the client to be overridden", async () => {
const { query } = setupSimpleCase();

Expand Down
2 changes: 0 additions & 2 deletions src/react/hooks/useBackgroundQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,6 @@ export function useBackgroundQuery<
updateWrappedQueryRef(wrappedQueryRef, promise);
}

React.useEffect(() => queryRef.retain(), [queryRef]);

const fetchMore: FetchMoreFunction<TData, TVariables> = React.useCallback(
(options) => {
const promise = queryRef.fetchMore(options as FetchMoreQueryOptions<any>);
Expand Down

0 comments on commit 6d46ab9

Please sign in to comment.