Skip to content

Commit

Permalink
fix(useBackgroundQuery): better usage tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
PiR1 committed Mar 20, 2024
1 parent ffd7211 commit b364b10
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 28 deletions.
53 changes: 53 additions & 0 deletions src/react/hooks/__tests__/useBackgroundQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ function createErrorProfiler<TData = unknown>() {
},
});
}

function createDefaultProfiler<TData = unknown>() {
return createProfiler({
initialSnapshot: {
Expand Down Expand Up @@ -562,6 +563,53 @@ it("does not prematurely dispose of the queryRef when using strict mode", async
expect(client).toHaveSuspenseCacheEntryUsing(query);
});

it("disposes of the queryRef when unmounting before it is used by useReadQuery even if it has been rerendered", async () => {
const { query, mocks } = setupSimpleCase();
const client = new ApolloClient({
link: new MockLink(mocks),
cache: new InMemoryCache(),
});
const user = userEvent.setup();

const Profiler = createDefaultProfiler<SimpleCaseData>();

function App() {
useTrackRenders();
useBackgroundQuery(query);

const [a, setA] = React.useState(0);

return (
<>
<button onClick={() => setA(a + 1)}>Increment</button>
</>
);
}

const { unmount } = renderWithClient(<App />, {
client,
wrapper: Profiler,
});
const button = screen.getByText("Increment");

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

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

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

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

await wait(0);

unmount();
await wait(0);
expect(client.getObservableQueries().size).toBe(0);
});

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

Expand Down Expand Up @@ -1101,6 +1149,7 @@ it("works with startTransition to change variables", async () => {
completed: boolean;
};
}

const user = userEvent.setup();

const query: TypedDocumentNode<Data, Variables> = gql`
Expand Down Expand Up @@ -4305,6 +4354,7 @@ describe("refetch", () => {
completed: boolean;
};
}

const user = userEvent.setup();

const query: TypedDocumentNode<Data, Variables> = gql`
Expand Down Expand Up @@ -4553,6 +4603,7 @@ describe("refetch", () => {
completed: boolean;
};
}

const user = userEvent.setup();

const query: TypedDocumentNode<Data, Variables> = gql`
Expand Down Expand Up @@ -5162,9 +5213,11 @@ describe("fetchMore", () => {
name: string;
completed: boolean;
}

interface Data {
todos: Todo[];
}

const user = userEvent.setup();

const query: TypedDocumentNode<Data, Variables> = gql`
Expand Down
7 changes: 3 additions & 4 deletions src/react/hooks/useBackgroundQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,8 @@ function _useBackgroundQuery<
...([] as any[]).concat(queryKey),
];

const queryRef = suspenseCache.getQueryRef(
cacheKey,
() => client.watchQuery(watchQueryOptions as WatchQueryOptions<any, any>),
true
const queryRef = suspenseCache.getQueryRef(cacheKey, () =>
client.watchQuery(watchQueryOptions as WatchQueryOptions<any, any>)
);

const [wrappedQueryRef, setWrappedQueryRef] = React.useState(
Expand Down Expand Up @@ -264,6 +262,7 @@ function _useBackgroundQuery<
);

React.useEffect(() => {
queryRef.newUsage();
return () => {
queryRef.disposeOnUnmount();
};
Expand Down
25 changes: 14 additions & 11 deletions src/react/internal/cache/QueryReference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,19 +160,17 @@ export class InternalQueryReference<TData = unknown> {
private reject: ((error: unknown) => void) | undefined;

private references = 0;
private nbOfUse = 0;
private numberOfUse = 0;

constructor(
observable: ObservableQuery<TData, any>,
options: InternalQueryReferenceOptions,
trackUses: boolean = false
options: InternalQueryReferenceOptions
) {
this.handleNext = this.handleNext.bind(this);
this.handleError = this.handleError.bind(this);
this.dispose = this.dispose.bind(this);
this.observable = observable;
this.autoDisposeTimeoutMs = options.autoDisposeTimeoutMs ?? 30_000;
this.nbOfUse = trackUses ? 1 : 0;

if (options.onDispose) {
this.onDispose = options.onDispose;
Expand Down Expand Up @@ -260,16 +258,21 @@ export class InternalQueryReference<TData = unknown> {
}

newUsage() {
this.nbOfUse++;
clearTimeout(this.autoDisposeTimeoutId);
this.startDisposeTimer();
this.numberOfUse++;
if (!this.references) {
clearTimeout(this.autoDisposeTimeoutId);
this.startDisposeTimer();
}
}

disposeOnUnmount() {
this.nbOfUse--;
if (!this.nbOfUse && !this.references) {
this.dispose();
}
this.numberOfUse--;
// Wait before fully disposing in case the app is running in strict mode.
setTimeout(() => {
if (!this.numberOfUse && !this.references) {
this.dispose();
}
});
}

didChangeOptions(watchQueryOptions: ObservedOptions) {
Expand Down
19 changes: 6 additions & 13 deletions src/react/internal/cache/SuspenseCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,19 @@ export class SuspenseCache {

getQueryRef<TData = any>(
cacheKey: CacheKey,
createObservable: () => ObservableQuery<TData>,
trackUses: boolean = false
createObservable: () => ObservableQuery<TData>
) {
const ref = this.queryRefs.lookupArray(cacheKey) as {
current?: InternalQueryReference<TData>;
};

if (!ref.current) {
ref.current = new InternalQueryReference(
createObservable(),
{
autoDisposeTimeoutMs: this.options.autoDisposeTimeoutMs,
onDispose: () => {
delete ref.current;
},
ref.current = new InternalQueryReference(createObservable(), {
autoDisposeTimeoutMs: this.options.autoDisposeTimeoutMs,
onDispose: () => {
delete ref.current;
},
trackUses
);
} else if (trackUses) {
ref.current.newUsage();
});
}

return ref.current;
Expand Down

0 comments on commit b364b10

Please sign in to comment.