Skip to content

Commit 70406bf

Browse files
add ignoreResults option to useSubscription (#11921)
* 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]>
1 parent 2941824 commit 70406bf

10 files changed

+341
-9
lines changed

.api-reports/api-report-react.api.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ export interface BaseSubscriptionOptions<TData = any, TVariables extends Operati
388388
context?: Context;
389389
// Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts
390390
fetchPolicy?: FetchPolicy;
391+
ignoreResults?: boolean;
391392
onComplete?: () => void;
392393
onData?: (options: OnDataOptions<TData>) => any;
393394
onError?: (error: ApolloError) => void;
@@ -1919,7 +1920,7 @@ export interface SubscriptionCurrentObservable {
19191920
subscription?: Subscription;
19201921
}
19211922

1922-
// @public (undocumented)
1923+
// @public @deprecated (undocumented)
19231924
export interface SubscriptionDataOptions<TData = any, TVariables extends OperationVariables = OperationVariables> extends BaseSubscriptionOptions<TData, TVariables> {
19241925
// (undocumented)
19251926
children?: null | ((result: SubscriptionResult<TData>) => ReactTypes.ReactNode);

.api-reports/api-report-react_components.api.md

+1
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ interface BaseSubscriptionOptions<TData = any, TVariables extends OperationVaria
336336
context?: DefaultContext;
337337
// Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts
338338
fetchPolicy?: FetchPolicy;
339+
ignoreResults?: boolean;
339340
onComplete?: () => void;
340341
// Warning: (ae-forgotten-export) The symbol "OnDataOptions" needs to be exported by the entry point index.d.ts
341342
onData?: (options: OnDataOptions<TData>) => any;

.api-reports/api-report-react_hooks.api.md

+1
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ interface BaseSubscriptionOptions<TData = any, TVariables extends OperationVaria
359359
context?: DefaultContext;
360360
// Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts
361361
fetchPolicy?: FetchPolicy;
362+
ignoreResults?: boolean;
362363
onComplete?: () => void;
363364
// Warning: (ae-forgotten-export) The symbol "OnDataOptions" needs to be exported by the entry point index.d.ts
364365
onData?: (options: OnDataOptions<TData>) => any;

.api-reports/api-report.api.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ export interface BaseSubscriptionOptions<TData = any, TVariables extends Operati
359359
client?: ApolloClient<object>;
360360
context?: DefaultContext;
361361
fetchPolicy?: FetchPolicy;
362+
ignoreResults?: boolean;
362363
onComplete?: () => void;
363364
onData?: (options: OnDataOptions<TData>) => any;
364365
onError?: (error: ApolloError) => void;
@@ -2551,7 +2552,7 @@ export interface SubscriptionCurrentObservable {
25512552
subscription?: ObservableSubscription;
25522553
}
25532554

2554-
// @public (undocumented)
2555+
// @public @deprecated (undocumented)
25552556
export interface SubscriptionDataOptions<TData = any, TVariables extends OperationVariables = OperationVariables> extends BaseSubscriptionOptions<TData, TVariables> {
25562557
// (undocumented)
25572558
children?: null | ((result: SubscriptionResult<TData>) => ReactTypes.ReactNode);

.changeset/unlucky-birds-press.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client": patch
3+
---
4+
5+
add `ignoreResults` option to `useSubscription`

.size-limits.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"dist/apollo-client.min.cjs": 39971,
2+
"dist/apollo-client.min.cjs": 40015,
33
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32903
44
}

src/react/hooks/__tests__/useSubscription.test.tsx

+291
Original file line numberDiff line numberDiff line change
@@ -1455,6 +1455,297 @@ describe("`restart` callback", () => {
14551455
});
14561456
});
14571457

1458+
describe("ignoreResults", () => {
1459+
const subscription = gql`
1460+
subscription {
1461+
car {
1462+
make
1463+
}
1464+
}
1465+
`;
1466+
1467+
const results = ["Audi", "BMW"].map((make) => ({
1468+
result: { data: { car: { make } } },
1469+
}));
1470+
1471+
it("should not rerender when ignoreResults is true, but will call `onData` and `onComplete`", async () => {
1472+
const link = new MockSubscriptionLink();
1473+
const client = new ApolloClient({
1474+
link,
1475+
cache: new Cache({ addTypename: false }),
1476+
});
1477+
1478+
const onData = jest.fn((() => {}) as SubscriptionHookOptions["onData"]);
1479+
const onError = jest.fn((() => {}) as SubscriptionHookOptions["onError"]);
1480+
const onComplete = jest.fn(
1481+
(() => {}) as SubscriptionHookOptions["onComplete"]
1482+
);
1483+
const ProfiledHook = profileHook(() =>
1484+
useSubscription(subscription, {
1485+
ignoreResults: true,
1486+
onData,
1487+
onError,
1488+
onComplete,
1489+
})
1490+
);
1491+
render(<ProfiledHook />, {
1492+
wrapper: ({ children }) => (
1493+
<ApolloProvider client={client}>{children}</ApolloProvider>
1494+
),
1495+
});
1496+
1497+
const snapshot = await ProfiledHook.takeSnapshot();
1498+
expect(snapshot).toStrictEqual({
1499+
loading: false,
1500+
error: undefined,
1501+
data: undefined,
1502+
variables: undefined,
1503+
restart: expect.any(Function),
1504+
});
1505+
link.simulateResult(results[0]);
1506+
1507+
await waitFor(() => {
1508+
expect(onData).toHaveBeenCalledTimes(1);
1509+
expect(onData).toHaveBeenLastCalledWith(
1510+
expect.objectContaining({
1511+
data: {
1512+
data: results[0].result.data,
1513+
error: undefined,
1514+
loading: false,
1515+
variables: undefined,
1516+
},
1517+
})
1518+
);
1519+
expect(onError).toHaveBeenCalledTimes(0);
1520+
expect(onComplete).toHaveBeenCalledTimes(0);
1521+
});
1522+
1523+
link.simulateResult(results[1], true);
1524+
await waitFor(() => {
1525+
expect(onData).toHaveBeenCalledTimes(2);
1526+
expect(onData).toHaveBeenLastCalledWith(
1527+
expect.objectContaining({
1528+
data: {
1529+
data: results[1].result.data,
1530+
error: undefined,
1531+
loading: false,
1532+
variables: undefined,
1533+
},
1534+
})
1535+
);
1536+
expect(onError).toHaveBeenCalledTimes(0);
1537+
expect(onComplete).toHaveBeenCalledTimes(1);
1538+
});
1539+
1540+
await expect(ProfiledHook).not.toRerender();
1541+
});
1542+
1543+
it("should not rerender when ignoreResults is true and an error occurs", async () => {
1544+
const link = new MockSubscriptionLink();
1545+
const client = new ApolloClient({
1546+
link,
1547+
cache: new Cache({ addTypename: false }),
1548+
});
1549+
1550+
const onData = jest.fn((() => {}) as SubscriptionHookOptions["onData"]);
1551+
const onError = jest.fn((() => {}) as SubscriptionHookOptions["onError"]);
1552+
const onComplete = jest.fn(
1553+
(() => {}) as SubscriptionHookOptions["onComplete"]
1554+
);
1555+
const ProfiledHook = profileHook(() =>
1556+
useSubscription(subscription, {
1557+
ignoreResults: true,
1558+
onData,
1559+
onError,
1560+
onComplete,
1561+
})
1562+
);
1563+
render(<ProfiledHook />, {
1564+
wrapper: ({ children }) => (
1565+
<ApolloProvider client={client}>{children}</ApolloProvider>
1566+
),
1567+
});
1568+
1569+
const snapshot = await ProfiledHook.takeSnapshot();
1570+
expect(snapshot).toStrictEqual({
1571+
loading: false,
1572+
error: undefined,
1573+
data: undefined,
1574+
variables: undefined,
1575+
restart: expect.any(Function),
1576+
});
1577+
link.simulateResult(results[0]);
1578+
1579+
await waitFor(() => {
1580+
expect(onData).toHaveBeenCalledTimes(1);
1581+
expect(onData).toHaveBeenLastCalledWith(
1582+
expect.objectContaining({
1583+
data: {
1584+
data: results[0].result.data,
1585+
error: undefined,
1586+
loading: false,
1587+
variables: undefined,
1588+
},
1589+
})
1590+
);
1591+
expect(onError).toHaveBeenCalledTimes(0);
1592+
expect(onComplete).toHaveBeenCalledTimes(0);
1593+
});
1594+
1595+
const error = new Error("test");
1596+
link.simulateResult({ error });
1597+
await waitFor(() => {
1598+
expect(onData).toHaveBeenCalledTimes(1);
1599+
expect(onError).toHaveBeenCalledTimes(1);
1600+
expect(onError).toHaveBeenLastCalledWith(error);
1601+
expect(onComplete).toHaveBeenCalledTimes(0);
1602+
});
1603+
1604+
await expect(ProfiledHook).not.toRerender();
1605+
});
1606+
1607+
it("can switch from `ignoreResults: true` to `ignoreResults: false` and will start rerendering, without creating a new subscription", async () => {
1608+
const subscriptionCreated = jest.fn();
1609+
const link = new MockSubscriptionLink();
1610+
link.onSetup(subscriptionCreated);
1611+
const client = new ApolloClient({
1612+
link,
1613+
cache: new Cache({ addTypename: false }),
1614+
});
1615+
1616+
const onData = jest.fn((() => {}) as SubscriptionHookOptions["onData"]);
1617+
const ProfiledHook = profileHook(
1618+
({ ignoreResults }: { ignoreResults: boolean }) =>
1619+
useSubscription(subscription, {
1620+
ignoreResults,
1621+
onData,
1622+
})
1623+
);
1624+
const { rerender } = render(<ProfiledHook ignoreResults={true} />, {
1625+
wrapper: ({ children }) => (
1626+
<ApolloProvider client={client}>{children}</ApolloProvider>
1627+
),
1628+
});
1629+
expect(subscriptionCreated).toHaveBeenCalledTimes(1);
1630+
1631+
{
1632+
const snapshot = await ProfiledHook.takeSnapshot();
1633+
expect(snapshot).toStrictEqual({
1634+
loading: false,
1635+
error: undefined,
1636+
data: undefined,
1637+
variables: undefined,
1638+
restart: expect.any(Function),
1639+
});
1640+
expect(onData).toHaveBeenCalledTimes(0);
1641+
}
1642+
link.simulateResult(results[0]);
1643+
await expect(ProfiledHook).not.toRerender({ timeout: 20 });
1644+
expect(onData).toHaveBeenCalledTimes(1);
1645+
1646+
rerender(<ProfiledHook ignoreResults={false} />);
1647+
{
1648+
const snapshot = await ProfiledHook.takeSnapshot();
1649+
expect(snapshot).toStrictEqual({
1650+
loading: false,
1651+
error: undefined,
1652+
// `data` appears immediately after changing to `ignoreResults: false`
1653+
data: results[0].result.data,
1654+
variables: undefined,
1655+
restart: expect.any(Function),
1656+
});
1657+
// `onData` should not be called again for the same result
1658+
expect(onData).toHaveBeenCalledTimes(1);
1659+
}
1660+
1661+
link.simulateResult(results[1]);
1662+
{
1663+
const snapshot = await ProfiledHook.takeSnapshot();
1664+
expect(snapshot).toStrictEqual({
1665+
loading: false,
1666+
error: undefined,
1667+
data: results[1].result.data,
1668+
variables: undefined,
1669+
restart: expect.any(Function),
1670+
});
1671+
expect(onData).toHaveBeenCalledTimes(2);
1672+
}
1673+
// a second subscription should not have been started
1674+
expect(subscriptionCreated).toHaveBeenCalledTimes(1);
1675+
});
1676+
it("can switch from `ignoreResults: false` to `ignoreResults: true` and will stop rerendering, without creating a new subscription", async () => {
1677+
const subscriptionCreated = jest.fn();
1678+
const link = new MockSubscriptionLink();
1679+
link.onSetup(subscriptionCreated);
1680+
const client = new ApolloClient({
1681+
link,
1682+
cache: new Cache({ addTypename: false }),
1683+
});
1684+
1685+
const onData = jest.fn((() => {}) as SubscriptionHookOptions["onData"]);
1686+
const ProfiledHook = profileHook(
1687+
({ ignoreResults }: { ignoreResults: boolean }) =>
1688+
useSubscription(subscription, {
1689+
ignoreResults,
1690+
onData,
1691+
})
1692+
);
1693+
const { rerender } = render(<ProfiledHook ignoreResults={false} />, {
1694+
wrapper: ({ children }) => (
1695+
<ApolloProvider client={client}>{children}</ApolloProvider>
1696+
),
1697+
});
1698+
expect(subscriptionCreated).toHaveBeenCalledTimes(1);
1699+
1700+
{
1701+
const snapshot = await ProfiledHook.takeSnapshot();
1702+
expect(snapshot).toStrictEqual({
1703+
loading: true,
1704+
error: undefined,
1705+
data: undefined,
1706+
variables: undefined,
1707+
restart: expect.any(Function),
1708+
});
1709+
expect(onData).toHaveBeenCalledTimes(0);
1710+
}
1711+
link.simulateResult(results[0]);
1712+
{
1713+
const snapshot = await ProfiledHook.takeSnapshot();
1714+
expect(snapshot).toStrictEqual({
1715+
loading: false,
1716+
error: undefined,
1717+
data: results[0].result.data,
1718+
variables: undefined,
1719+
restart: expect.any(Function),
1720+
});
1721+
expect(onData).toHaveBeenCalledTimes(1);
1722+
}
1723+
await expect(ProfiledHook).not.toRerender({ timeout: 20 });
1724+
1725+
rerender(<ProfiledHook ignoreResults={true} />);
1726+
{
1727+
const snapshot = await ProfiledHook.takeSnapshot();
1728+
expect(snapshot).toStrictEqual({
1729+
loading: false,
1730+
error: undefined,
1731+
// switching back to the default `ignoreResults: true` return value
1732+
data: undefined,
1733+
variables: undefined,
1734+
restart: expect.any(Function),
1735+
});
1736+
// `onData` should not be called again
1737+
expect(onData).toHaveBeenCalledTimes(1);
1738+
}
1739+
1740+
link.simulateResult(results[1]);
1741+
await expect(ProfiledHook).not.toRerender({ timeout: 20 });
1742+
expect(onData).toHaveBeenCalledTimes(2);
1743+
1744+
// a second subscription should not have been started
1745+
expect(subscriptionCreated).toHaveBeenCalledTimes(1);
1746+
});
1747+
});
1748+
14581749
describe.skip("Type Tests", () => {
14591750
test("NoInfer prevents adding arbitrary additional variables", () => {
14601751
const typedNode = {} as TypedDocumentNode<{ foo: string }, { bar: number }>;

0 commit comments

Comments
 (0)