Add array support to useFragment, useSuspenseFragment, and client.watchFragment#12971
Add array support to useFragment, useSuspenseFragment, and client.watchFragment#12971jerelmiller merged 203 commits intorelease-4.1from
useFragment, useSuspenseFragment, and client.watchFragment#12971Conversation
🦋 Changeset detectedLatest commit: bc06591 The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ Docs preview readyThe preview is ready to be viewed. View the preview File Changes 0 new, 11 changed, 0 removedBuild ID: 102daf5dcb2399ff53151d2e |
commit: |
| link: ApolloLink.empty(), | ||
| }); | ||
|
|
||
| const observable = client.watchFragment({ |
There was a problem hiding this comment.
Should we complete the observable if its the from: null observable?
There was a problem hiding this comment.
I think we should keep it the same for all calls to watchFragment, so never-completing. I can imagine someone resubscribing in an infinite loop or something similar otherwise.
There was a problem hiding this comment.
Pull Request Overview
This PR adds array support to useFragment, useSuspenseFragment, and client.watchFragment, allowing developers to watch multiple fragment entities simultaneously. The from option now accepts arrays, returning an array of data where each index corresponds to the from array index.
Key changes:
- Added
combineLatestBatchedutility for efficient Observable batching - Enhanced fragment watching to support arrays with proper type inference
- Added
getCurrentResultmethod to fragment observables
Reviewed Changes
Copilot reviewed 38 out of 39 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utilities/internal/combineLatestBatched.ts | New utility for batching observable updates from array items sharing the same observable |
| src/utilities/DeepPartial.ts | Removed undefined from array item types in DeepPartial |
| src/cache/core/cache.ts | Core implementation of array support for watchFragment with deduplication and batching |
| src/core/ApolloClient.ts | Added overloads and getCurrentResult method for watchFragment |
| src/react/hooks/useFragment.ts | Refactored to use watchFragment observable with array support |
| src/react/hooks/useSuspenseFragment.ts | Added array support with suspense behavior |
| src/testing/matchers/*.ts | New test matchers for fragment watches validation |
| docs/source/data/fragments.mdx | Documentation updates for array support |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| // Unfortunately we forgot to allow for `null` on watchFragment in 4.0 | ||
| // when `from` is a single record. As such, we need to fallback to {} | ||
| // when diff.result is null to maintain backwards compatibility. We | ||
| // should plan to change this in v5. We do howeever support `null` if | ||
| // `from` is explicitly `null`. | ||
| // | ||
| // NOTE: Using `from` with an array will maintain `null` properly | ||
| // without the need for a similar fallback since watchFragment with | ||
| // arrays is new functionality in v4. |
There was a problem hiding this comment.
[nitpick] The backwards compatibility fallback (?? {}) for non-null from values deserves a more prominent comment explaining why this behavior differs from the array case and when it can be removed (v5).
| // Unfortunately we forgot to allow for `null` on watchFragment in 4.0 | |
| // when `from` is a single record. As such, we need to fallback to {} | |
| // when diff.result is null to maintain backwards compatibility. We | |
| // should plan to change this in v5. We do howeever support `null` if | |
| // `from` is explicitly `null`. | |
| // | |
| // NOTE: Using `from` with an array will maintain `null` properly | |
| // without the need for a similar fallback since watchFragment with | |
| // arrays is new functionality in v4. | |
| // TODO(v5): Remove backwards compatibility fallback for non-null `from` values. | |
| // In Apollo Client v4, we did not allow `null` for watchFragment when `from` is a single record. | |
| // To maintain backwards compatibility, we fallback to `{}` when `diff.result` is `null` and `from` is not `null`. | |
| // This differs from the array case (new in v4), which properly supports `null` values and does not require this fallback. | |
| // The fallback can be removed in v5, at which point `null` will be returned for `diff.result` when appropriate. | |
| // If `from` is explicitly `null`, we do support returning `null` as the result. |
| let currentResult: ApolloClient.WatchFragmentResult<any>; | ||
| let stableMaskedResult: ApolloClient.WatchFragmentResult<any>; | ||
|
|
||
| return Object.assign(observable.pipe(map(mask)), { |
There was a problem hiding this comment.
Could it be that we need another distinctUntilChanged here?
In dev, something could change upstream and then be filtered out, resulting in a "no change" situation.
In that case, the tracking of currentResult below might also not be enough.
There was a problem hiding this comment.
something could change upstream and then be filtered out, resulting in a "no change" situation.
Just to clarify, by "upstream" do you mean data that changes in a nested fragment that would otherwise be masked?
I think this is covered by the document transform we apply to the fragment document before passing it to cache.watchFragment. That transform removes any fragment spreads without @unmask from the fragment document, resulting in only a document that includes fields that we care to watch. The maskFragment in __DEV__ is only to handle @unmask(mode: "migrate") where we want to apply warnings on field access.
There was a problem hiding this comment.
Ah, good call. For some reason I had assumed that we would only filter down the fragment in non-DEV mode 🤦
| }); | ||
|
|
||
| // ensure it changes identity when a new value is emitted | ||
| expect(observable.getCurrentResult()).not.toBe(lastResult); |
There was a problem hiding this comment.
Can we have a test the other direction, too? Emit first, then call getCurrentResult, check for referential equality.
There was a problem hiding this comment.
Do lines 209-214 cover that, or are you looking to check the value after its changed?
There was a problem hiding this comment.
No, not covered yet. A bit unfortunate that the comment is only attached to this line, it was more a comment for the whole test :)
This test is "call getCurrentResult, subscribe, check referential equality". I'd like to have "subscribe, call getCurrentResult, check referential equality", too - doing things in the other direction and seeing them be referentially equal, too - both starting from a "new, unsubscribed" observable.
There was a problem hiding this comment.
Added some more assertions to check that this 2nd emitted result is referentially stable with the value emitted from the observable: e677cfd
There was a problem hiding this comment.
I tweaked the order of assertions in 6a2dc58 that get the result from the observable, then check it against getCurrentResult. I think this covers it?
phryneas
left a comment
There was a problem hiding this comment.
Looks good to me, let's get this out there 🚀
…t.watchFragment` (#12971) Co-authored-by: Lenz Weber-Tronic <[email protected]> Co-authored-by: jerelmiller <[email protected]>
Closes apollographql/apollo-feature-requests#452
Closes apollographql/apollo-feature-requests#358
Add support for lists with
useFragment,useSuspenseFragment, andclient.watchFragment.