Skip to content

Commit 21c3f08

Browse files
authored
[Data masking] Allow null as a valid from value (#12131)
1 parent 1e7d009 commit 21c3f08

File tree

8 files changed

+137
-26
lines changed

8 files changed

+137
-26
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2251,7 +2251,7 @@ export interface UseFragmentOptions<TData, TVars> extends Omit<Cache_2.DiffOptio
22512251
// Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts
22522252
//
22532253
// (undocumented)
2254-
from: StoreObject | Reference | FragmentType<NoInfer_2<TData>> | string;
2254+
from: StoreObject | Reference | FragmentType<NoInfer_2<TData>> | string | null;
22552255
// (undocumented)
22562256
optimistic?: boolean;
22572257
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2075,7 +2075,7 @@ export interface UseFragmentOptions<TData, TVars> extends Omit<Cache_2.DiffOptio
20752075
// Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts
20762076
//
20772077
// (undocumented)
2078-
from: StoreObject | Reference | FragmentType<NoInfer_2<TData>> | string;
2078+
from: StoreObject | Reference | FragmentType<NoInfer_2<TData>> | string | null;
20792079
// (undocumented)
20802080
optimistic?: boolean;
20812081
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2138,7 +2138,7 @@ interface UseFragmentOptions<TData, TVars> extends Omit<Cache_2.DiffOptions<NoIn
21382138
// Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts
21392139
//
21402140
// (undocumented)
2141-
from: StoreObject | Reference | FragmentType<NoInfer_2<TData>> | string;
2141+
from: StoreObject | Reference | FragmentType<NoInfer_2<TData>> | string | null;
21422142
// (undocumented)
21432143
optimistic?: boolean;
21442144
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2924,7 +2924,7 @@ export function useFragment<TData = any, TVars = OperationVariables>(options: Us
29242924
export interface UseFragmentOptions<TData, TVars> extends Omit<Cache_2.DiffOptions<NoInfer_2<TData>, NoInfer_2<TVars>>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit<Cache_2.ReadFragmentOptions<TData, TVars>, "id" | "variables" | "returnPartialData"> {
29252925
client?: ApolloClient<any>;
29262926
// (undocumented)
2927-
from: StoreObject | Reference | FragmentType<NoInfer_2<TData>> | string;
2927+
from: StoreObject | Reference | FragmentType<NoInfer_2<TData>> | string | null;
29282928
// (undocumented)
29292929
optimistic?: boolean;
29302930
}

.changeset/long-zoos-ring.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client": minor
3+
---
4+
5+
Allow `null` as a valid `from` value in `useFragment`.

.size-limits.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"dist/apollo-client.min.cjs": 41573,
3-
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34361
2+
"dist/apollo-client.min.cjs": 41601,
3+
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34359
44
}

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

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1646,6 +1646,95 @@ describe("useFragment", () => {
16461646
}
16471647
});
16481648

1649+
it("allows `null` as valid `from` value without warning", async () => {
1650+
using _ = spyOnConsole("warn");
1651+
1652+
interface Fragment {
1653+
age: number;
1654+
}
1655+
1656+
const fragment: TypedDocumentNode<Fragment, never> = gql`
1657+
fragment UserFields on User {
1658+
age
1659+
}
1660+
`;
1661+
1662+
const client = new ApolloClient({ cache: new InMemoryCache() });
1663+
1664+
const { takeSnapshot } = renderHookToSnapshotStream(
1665+
() => useFragment({ fragment, from: null }),
1666+
{
1667+
wrapper: ({ children }) => (
1668+
<ApolloProvider client={client}>{children}</ApolloProvider>
1669+
),
1670+
}
1671+
);
1672+
1673+
{
1674+
const { data, complete } = await takeSnapshot();
1675+
1676+
expect(data).toEqual({});
1677+
expect(complete).toBe(false);
1678+
}
1679+
1680+
expect(console.warn).not.toHaveBeenCalled();
1681+
});
1682+
1683+
it("properly handles changing from null to valid from value", async () => {
1684+
using _ = spyOnConsole("warn");
1685+
1686+
interface Fragment {
1687+
__typename: "User";
1688+
id: string;
1689+
age: number;
1690+
}
1691+
1692+
const fragment: TypedDocumentNode<Fragment, never> = gql`
1693+
fragment UserFields on User {
1694+
__typename
1695+
id
1696+
age
1697+
}
1698+
`;
1699+
1700+
const client = new ApolloClient({ cache: new InMemoryCache() });
1701+
1702+
client.writeFragment({
1703+
fragment,
1704+
data: {
1705+
__typename: "User",
1706+
id: "1",
1707+
age: 30,
1708+
},
1709+
});
1710+
1711+
const { takeSnapshot, rerender } = renderHookToSnapshotStream(
1712+
({ from }) => useFragment({ fragment, from }),
1713+
{
1714+
initialProps: { from: null as UseFragmentOptions<any, never>["from"] },
1715+
wrapper: ({ children }) => (
1716+
<ApolloProvider client={client}>{children}</ApolloProvider>
1717+
),
1718+
}
1719+
);
1720+
1721+
{
1722+
const { data, complete } = await takeSnapshot();
1723+
1724+
expect(data).toEqual({});
1725+
expect(complete).toBe(false);
1726+
}
1727+
1728+
rerender({ from: { __typename: "User", id: "1" } });
1729+
1730+
{
1731+
const { data, complete } = await takeSnapshot();
1732+
1733+
expect(data).toEqual({ __typename: "User", id: "1", age: 30 });
1734+
expect(complete).toBe(true);
1735+
}
1736+
});
1737+
16491738
describe("tests with incomplete data", () => {
16501739
let cache: InMemoryCache, wrapper: React.FunctionComponent;
16511740
const ItemFragment = gql`
@@ -2327,7 +2416,7 @@ describe.skip("Type Tests", () => {
23272416

23282417
test("UseFragmentOptions interface shape", <TData, TVars>() => {
23292418
expectTypeOf<UseFragmentOptions<TData, TVars>>().branded.toEqualTypeOf<{
2330-
from: string | StoreObject | Reference | FragmentType<TData>;
2419+
from: string | StoreObject | Reference | FragmentType<TData> | null;
23312420
fragment: DocumentNode | TypedDocumentNode<TData, TVars>;
23322421
fragmentName?: string;
23332422
optimistic?: boolean;

src/react/hooks/useFragment.ts

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export interface UseFragmentOptions<TData, TVars>
2525
Cache.ReadFragmentOptions<TData, TVars>,
2626
"id" | "variables" | "returnPartialData"
2727
> {
28-
from: StoreObject | Reference | FragmentType<NoInfer<TData>> | string;
28+
from: StoreObject | Reference | FragmentType<NoInfer<TData>> | string | null;
2929
// Override this field to make it optional (default: true).
3030
optimistic?: boolean;
3131
/**
@@ -73,7 +73,10 @@ function _useFragment<TData = any, TVars = OperationVariables>(
7373
// `stableOptions` and retrigger our subscription. If the cache identifier
7474
// stays the same between renders, we want to reuse the existing subscription.
7575
const id = React.useMemo(
76-
() => (typeof from === "string" ? from : cache.identify(from)),
76+
() =>
77+
typeof from === "string" ? from
78+
: from === null ? null
79+
: cache.identify(from),
7780
[cache, from]
7881
);
7982

@@ -83,6 +86,16 @@ function _useFragment<TData = any, TVars = OperationVariables>(
8386
// get the correct diff on the next render given new diffOptions
8487
const diff = React.useMemo(() => {
8588
const { fragment, fragmentName, from, optimistic = true } = stableOptions;
89+
90+
if (from === null) {
91+
return {
92+
result: diffToResult({
93+
result: {} as TData,
94+
complete: false,
95+
}),
96+
};
97+
}
98+
8699
const { cache } = client;
87100
const diff = cache.diff<TData>({
88101
...stableOptions,
@@ -111,24 +124,28 @@ function _useFragment<TData = any, TVars = OperationVariables>(
111124
React.useCallback(
112125
(forceUpdate) => {
113126
let lastTimeout = 0;
114-
const subscription = client.watchFragment(stableOptions).subscribe({
115-
next: (result) => {
116-
// Since `next` is called async by zen-observable, we want to avoid
117-
// unnecessarily rerendering this hook for the initial result
118-
// emitted from watchFragment which should be equal to
119-
// `diff.result`.
120-
if (equal(result, diff.result)) return;
121-
diff.result = result;
122-
// If we get another update before we've re-rendered, bail out of
123-
// the update and try again. This ensures that the relative timing
124-
// between useQuery and useFragment stays roughly the same as
125-
// fixed in https://github.com/apollographql/apollo-client/pull/11083
126-
clearTimeout(lastTimeout);
127-
lastTimeout = setTimeout(forceUpdate) as any;
128-
},
129-
});
127+
128+
const subscription =
129+
stableOptions.from === null ?
130+
null
131+
: client.watchFragment(stableOptions).subscribe({
132+
next: (result) => {
133+
// Since `next` is called async by zen-observable, we want to avoid
134+
// unnecessarily rerendering this hook for the initial result
135+
// emitted from watchFragment which should be equal to
136+
// `diff.result`.
137+
if (equal(result, diff.result)) return;
138+
diff.result = result;
139+
// If we get another update before we've re-rendered, bail out of
140+
// the update and try again. This ensures that the relative timing
141+
// between useQuery and useFragment stays roughly the same as
142+
// fixed in https://github.com/apollographql/apollo-client/pull/11083
143+
clearTimeout(lastTimeout);
144+
lastTimeout = setTimeout(forceUpdate) as any;
145+
},
146+
});
130147
return () => {
131-
subscription.unsubscribe();
148+
subscription?.unsubscribe();
132149
clearTimeout(lastTimeout);
133150
};
134151
},

0 commit comments

Comments
 (0)