Skip to content

Commit

Permalink
add ignoreResults option to useSubscription (#11921)
Browse files Browse the repository at this point in the history
* add `ignoreResults` option to `useSubscription`

* more tests

* changeset

* restore type, add deprecation, tweak tag

* Update src/react/hooks/useSubscription.ts

* reflect code change in comment

* review feedback

* Update src/react/types/types.documentation.ts

Co-authored-by: Jerel Miller <[email protected]>

* add clarification about resetting the return value when switching on `ignoreResults` later

* test fixup

---------

Co-authored-by: Jerel Miller <[email protected]>
  • Loading branch information
phryneas and jerelmiller authored Jul 8, 2024
1 parent 2941824 commit 70406bf
Show file tree
Hide file tree
Showing 10 changed files with 341 additions and 9 deletions.
3 changes: 2 additions & 1 deletion .api-reports/api-report-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ export interface BaseSubscriptionOptions<TData = any, TVariables extends Operati
context?: Context;
// Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts
fetchPolicy?: FetchPolicy;
ignoreResults?: boolean;
onComplete?: () => void;
onData?: (options: OnDataOptions<TData>) => any;
onError?: (error: ApolloError) => void;
Expand Down Expand Up @@ -1919,7 +1920,7 @@ export interface SubscriptionCurrentObservable {
subscription?: Subscription;
}

// @public (undocumented)
// @public @deprecated (undocumented)
export interface SubscriptionDataOptions<TData = any, TVariables extends OperationVariables = OperationVariables> extends BaseSubscriptionOptions<TData, TVariables> {
// (undocumented)
children?: null | ((result: SubscriptionResult<TData>) => ReactTypes.ReactNode);
Expand Down
1 change: 1 addition & 0 deletions .api-reports/api-report-react_components.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ interface BaseSubscriptionOptions<TData = any, TVariables extends OperationVaria
context?: DefaultContext;
// Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts
fetchPolicy?: FetchPolicy;
ignoreResults?: boolean;
onComplete?: () => void;
// Warning: (ae-forgotten-export) The symbol "OnDataOptions" needs to be exported by the entry point index.d.ts
onData?: (options: OnDataOptions<TData>) => any;
Expand Down
1 change: 1 addition & 0 deletions .api-reports/api-report-react_hooks.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ interface BaseSubscriptionOptions<TData = any, TVariables extends OperationVaria
context?: DefaultContext;
// Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts
fetchPolicy?: FetchPolicy;
ignoreResults?: boolean;
onComplete?: () => void;
// Warning: (ae-forgotten-export) The symbol "OnDataOptions" needs to be exported by the entry point index.d.ts
onData?: (options: OnDataOptions<TData>) => any;
Expand Down
3 changes: 2 additions & 1 deletion .api-reports/api-report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ export interface BaseSubscriptionOptions<TData = any, TVariables extends Operati
client?: ApolloClient<object>;
context?: DefaultContext;
fetchPolicy?: FetchPolicy;
ignoreResults?: boolean;
onComplete?: () => void;
onData?: (options: OnDataOptions<TData>) => any;
onError?: (error: ApolloError) => void;
Expand Down Expand Up @@ -2551,7 +2552,7 @@ export interface SubscriptionCurrentObservable {
subscription?: ObservableSubscription;
}

// @public (undocumented)
// @public @deprecated (undocumented)
export interface SubscriptionDataOptions<TData = any, TVariables extends OperationVariables = OperationVariables> extends BaseSubscriptionOptions<TData, TVariables> {
// (undocumented)
children?: null | ((result: SubscriptionResult<TData>) => ReactTypes.ReactNode);
Expand Down
5 changes: 5 additions & 0 deletions .changeset/unlucky-birds-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client": patch
---

add `ignoreResults` option to `useSubscription`
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": 39971,
"dist/apollo-client.min.cjs": 40015,
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32903
}
291 changes: 291 additions & 0 deletions src/react/hooks/__tests__/useSubscription.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1455,6 +1455,297 @@ describe("`restart` callback", () => {
});
});

describe("ignoreResults", () => {
const subscription = gql`
subscription {
car {
make
}
}
`;

const results = ["Audi", "BMW"].map((make) => ({
result: { data: { car: { make } } },
}));

it("should not rerender when ignoreResults is true, but will call `onData` and `onComplete`", async () => {
const link = new MockSubscriptionLink();
const client = new ApolloClient({
link,
cache: new Cache({ addTypename: false }),
});

const onData = jest.fn((() => {}) as SubscriptionHookOptions["onData"]);
const onError = jest.fn((() => {}) as SubscriptionHookOptions["onError"]);
const onComplete = jest.fn(
(() => {}) as SubscriptionHookOptions["onComplete"]
);
const ProfiledHook = profileHook(() =>
useSubscription(subscription, {
ignoreResults: true,
onData,
onError,
onComplete,
})
);
render(<ProfiledHook />, {
wrapper: ({ children }) => (
<ApolloProvider client={client}>{children}</ApolloProvider>
),
});

const snapshot = await ProfiledHook.takeSnapshot();
expect(snapshot).toStrictEqual({
loading: false,
error: undefined,
data: undefined,
variables: undefined,
restart: expect.any(Function),
});
link.simulateResult(results[0]);

await waitFor(() => {
expect(onData).toHaveBeenCalledTimes(1);
expect(onData).toHaveBeenLastCalledWith(
expect.objectContaining({
data: {
data: results[0].result.data,
error: undefined,
loading: false,
variables: undefined,
},
})
);
expect(onError).toHaveBeenCalledTimes(0);
expect(onComplete).toHaveBeenCalledTimes(0);
});

link.simulateResult(results[1], true);
await waitFor(() => {
expect(onData).toHaveBeenCalledTimes(2);
expect(onData).toHaveBeenLastCalledWith(
expect.objectContaining({
data: {
data: results[1].result.data,
error: undefined,
loading: false,
variables: undefined,
},
})
);
expect(onError).toHaveBeenCalledTimes(0);
expect(onComplete).toHaveBeenCalledTimes(1);
});

await expect(ProfiledHook).not.toRerender();
});

it("should not rerender when ignoreResults is true and an error occurs", async () => {
const link = new MockSubscriptionLink();
const client = new ApolloClient({
link,
cache: new Cache({ addTypename: false }),
});

const onData = jest.fn((() => {}) as SubscriptionHookOptions["onData"]);
const onError = jest.fn((() => {}) as SubscriptionHookOptions["onError"]);
const onComplete = jest.fn(
(() => {}) as SubscriptionHookOptions["onComplete"]
);
const ProfiledHook = profileHook(() =>
useSubscription(subscription, {
ignoreResults: true,
onData,
onError,
onComplete,
})
);
render(<ProfiledHook />, {
wrapper: ({ children }) => (
<ApolloProvider client={client}>{children}</ApolloProvider>
),
});

const snapshot = await ProfiledHook.takeSnapshot();
expect(snapshot).toStrictEqual({
loading: false,
error: undefined,
data: undefined,
variables: undefined,
restart: expect.any(Function),
});
link.simulateResult(results[0]);

await waitFor(() => {
expect(onData).toHaveBeenCalledTimes(1);
expect(onData).toHaveBeenLastCalledWith(
expect.objectContaining({
data: {
data: results[0].result.data,
error: undefined,
loading: false,
variables: undefined,
},
})
);
expect(onError).toHaveBeenCalledTimes(0);
expect(onComplete).toHaveBeenCalledTimes(0);
});

const error = new Error("test");
link.simulateResult({ error });
await waitFor(() => {
expect(onData).toHaveBeenCalledTimes(1);
expect(onError).toHaveBeenCalledTimes(1);
expect(onError).toHaveBeenLastCalledWith(error);
expect(onComplete).toHaveBeenCalledTimes(0);
});

await expect(ProfiledHook).not.toRerender();
});

it("can switch from `ignoreResults: true` to `ignoreResults: false` and will start rerendering, without creating a new subscription", async () => {
const subscriptionCreated = jest.fn();
const link = new MockSubscriptionLink();
link.onSetup(subscriptionCreated);
const client = new ApolloClient({
link,
cache: new Cache({ addTypename: false }),
});

const onData = jest.fn((() => {}) as SubscriptionHookOptions["onData"]);
const ProfiledHook = profileHook(
({ ignoreResults }: { ignoreResults: boolean }) =>
useSubscription(subscription, {
ignoreResults,
onData,
})
);
const { rerender } = render(<ProfiledHook ignoreResults={true} />, {
wrapper: ({ children }) => (
<ApolloProvider client={client}>{children}</ApolloProvider>
),
});
expect(subscriptionCreated).toHaveBeenCalledTimes(1);

{
const snapshot = await ProfiledHook.takeSnapshot();
expect(snapshot).toStrictEqual({
loading: false,
error: undefined,
data: undefined,
variables: undefined,
restart: expect.any(Function),
});
expect(onData).toHaveBeenCalledTimes(0);
}
link.simulateResult(results[0]);
await expect(ProfiledHook).not.toRerender({ timeout: 20 });
expect(onData).toHaveBeenCalledTimes(1);

rerender(<ProfiledHook ignoreResults={false} />);
{
const snapshot = await ProfiledHook.takeSnapshot();
expect(snapshot).toStrictEqual({
loading: false,
error: undefined,
// `data` appears immediately after changing to `ignoreResults: false`
data: results[0].result.data,
variables: undefined,
restart: expect.any(Function),
});
// `onData` should not be called again for the same result
expect(onData).toHaveBeenCalledTimes(1);
}

link.simulateResult(results[1]);
{
const snapshot = await ProfiledHook.takeSnapshot();
expect(snapshot).toStrictEqual({
loading: false,
error: undefined,
data: results[1].result.data,
variables: undefined,
restart: expect.any(Function),
});
expect(onData).toHaveBeenCalledTimes(2);
}
// a second subscription should not have been started
expect(subscriptionCreated).toHaveBeenCalledTimes(1);
});
it("can switch from `ignoreResults: false` to `ignoreResults: true` and will stop rerendering, without creating a new subscription", async () => {
const subscriptionCreated = jest.fn();
const link = new MockSubscriptionLink();
link.onSetup(subscriptionCreated);
const client = new ApolloClient({
link,
cache: new Cache({ addTypename: false }),
});

const onData = jest.fn((() => {}) as SubscriptionHookOptions["onData"]);
const ProfiledHook = profileHook(
({ ignoreResults }: { ignoreResults: boolean }) =>
useSubscription(subscription, {
ignoreResults,
onData,
})
);
const { rerender } = render(<ProfiledHook ignoreResults={false} />, {
wrapper: ({ children }) => (
<ApolloProvider client={client}>{children}</ApolloProvider>
),
});
expect(subscriptionCreated).toHaveBeenCalledTimes(1);

{
const snapshot = await ProfiledHook.takeSnapshot();
expect(snapshot).toStrictEqual({
loading: true,
error: undefined,
data: undefined,
variables: undefined,
restart: expect.any(Function),
});
expect(onData).toHaveBeenCalledTimes(0);
}
link.simulateResult(results[0]);
{
const snapshot = await ProfiledHook.takeSnapshot();
expect(snapshot).toStrictEqual({
loading: false,
error: undefined,
data: results[0].result.data,
variables: undefined,
restart: expect.any(Function),
});
expect(onData).toHaveBeenCalledTimes(1);
}
await expect(ProfiledHook).not.toRerender({ timeout: 20 });

rerender(<ProfiledHook ignoreResults={true} />);
{
const snapshot = await ProfiledHook.takeSnapshot();
expect(snapshot).toStrictEqual({
loading: false,
error: undefined,
// switching back to the default `ignoreResults: true` return value
data: undefined,
variables: undefined,
restart: expect.any(Function),
});
// `onData` should not be called again
expect(onData).toHaveBeenCalledTimes(1);
}

link.simulateResult(results[1]);
await expect(ProfiledHook).not.toRerender({ timeout: 20 });
expect(onData).toHaveBeenCalledTimes(2);

// a second subscription should not have been started
expect(subscriptionCreated).toHaveBeenCalledTimes(1);
});
});

describe.skip("Type Tests", () => {
test("NoInfer prevents adding arbitrary additional variables", () => {
const typedNode = {} as TypedDocumentNode<{ foo: string }, { bar: number }>;
Expand Down
Loading

0 comments on commit 70406bf

Please sign in to comment.