Skip to content

Add array support to useFragment, useSuspenseFragment, and client.watchFragment#12971

Merged
jerelmiller merged 203 commits intorelease-4.1from
jerel/use-fragment-list
Oct 27, 2025
Merged

Add array support to useFragment, useSuspenseFragment, and client.watchFragment#12971
jerelmiller merged 203 commits intorelease-4.1from
jerel/use-fragment-list

Conversation

@jerelmiller
Copy link
Copy Markdown
Member

@jerelmiller jerelmiller commented Oct 13, 2025

Closes apollographql/apollo-feature-requests#452
Closes apollographql/apollo-feature-requests#358

Add support for lists with useFragment, useSuspenseFragment, and client.watchFragment.

const result = useFragment({
  fragment,
  from: [item1, item2]
});

console.log(result); // { data: [{...}, {...}], dataState: "complete", complete: true }

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Oct 13, 2025

🦋 Changeset detected

Latest 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

@apollo-librarian
Copy link
Copy Markdown

apollo-librarian bot commented Oct 13, 2025

✅ Docs preview ready

The preview is ready to be viewed. View the preview

File Changes

0 new, 11 changed, 0 removed
* (developer-tools)/react/(latest)/caching/cache-configuration.mdx
* (developer-tools)/react/(latest)/data/fragments.mdx
* (developer-tools)/react/(latest)/data/mutations.mdx
* (developer-tools)/react/(latest)/data/persisted-queries.mdx
* (developer-tools)/react/(latest)/data/queries.mdx
* (developer-tools)/react/(latest)/development-testing/testing.mdx
* (developer-tools)/react/(latest)/integrations/react-native.mdx
* (developer-tools)/react/(latest)/migrating/apollo-client-4-migration.mdx
* (developer-tools)/react/(latest)/performance/server-side-rendering.mdx
* (developer-tools)/react/(latest)/VERSIONING_POLICY.md
* (developer-tools)/react/(latest)/versioning-policy.md

Build ID: 102daf5dcb2399ff53151d2e
Build Logs: View logs

URL: https://www.apollographql.com/docs/deploy-preview/102daf5dcb2399ff53151d2e

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Oct 13, 2025

npm i https://pkg.pr.new/apollographql/apollo-client/@apollo/client@12971

commit: 209c1f8

link: ApolloLink.empty(),
});

const observable = client.watchFragment({
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we complete the observable if its the from: null observable?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@phryneas phryneas requested a review from Copilot October 24, 2025 09:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 combineLatestBatched utility for efficient Observable batching
  • Enhanced fragment watching to support arrays with proper type inference
  • Added getCurrentResult method 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.

Comment on lines +508 to +516
// 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.
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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).

Suggested change
// 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.

Copilot uses AI. Check for mistakes.
let currentResult: ApolloClient.WatchFragmentResult<any>;
let stableMaskedResult: ApolloClient.WatchFragmentResult<any>;

return Object.assign(observable.pipe(map(mask)), {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a test the other direction, too? Emit first, then call getCurrentResult, check for referential equality.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do lines 209-214 cover that, or are you looking to check the value after its changed?

Copy link
Copy Markdown
Member

@phryneas phryneas Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some more assertions to check that this 2nd emitted result is referentially stable with the value emitted from the observable: e677cfd

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member

@phryneas phryneas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, let's get this out there 🚀

@jerelmiller jerelmiller merged commit d11eb40 into release-4.1 Oct 27, 2025
31 of 33 checks passed
@jerelmiller jerelmiller deleted the jerel/use-fragment-list branch October 27, 2025 15:35
jerelmiller added a commit that referenced this pull request Nov 17, 2025
…t.watchFragment` (#12971)

Co-authored-by: Lenz Weber-Tronic <[email protected]>
Co-authored-by: jerelmiller <[email protected]>
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Nov 27, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants