Skip to content

Add a dataState enum to ApolloQueryResult #12350

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 49 commits into
base: release-4.0
Choose a base branch
from

Conversation

jerelmiller
Copy link
Member

@jerelmiller jerelmiller commented Feb 6, 2025

Closes #12344
Closes #12345

Adds a new dataState enum returned from ApolloQueryResult that describes the state of the data property. ApolloQueryResult is now a discriminated union that better describes the value of data when in a particular state which provides additional type safety.

This new enum is now able to track whether the is waiting for additional chunks when used with @defer which can be useful to determine whether the full result has streamed in or not.

Copy link

changeset-bot bot commented Feb 6, 2025

🦋 Changeset detected

Latest commit: 219d890

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@apollo/client Major

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

@svc-apollo-docs
Copy link

svc-apollo-docs commented Feb 6, 2025

⚠️ Docs preview not attached to branch

The preview was not built because the PR's base branch release-4.0 is not in the list of sources.

An Apollo team member can comment one of the following commands to dictate which branch to attach the preview to:

  • !docs set-base-branch version-2.6
  • !docs set-base-branch main

Build ID: 221b2b08d1fed19c8ab7bee8

Copy link

pkg-pr-new bot commented Feb 6, 2025

npm i https://pkg.pr.new/@apollo/client@12350

commit: 5f6b041

* - `complete`: `data` is a fully satisfied query result fulfilled
* either from the cache or network.
*/
dataState: "complete";
Copy link
Member Author

Choose a reason for hiding this comment

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

Let the bike shedding commence!

Copy link

netlify bot commented Feb 6, 2025

Deploy Preview for apollo-client-docs ready!

Name Link
🔨 Latest commit 5f6b041
🔍 Latest deploy log https://app.netlify.com/sites/apollo-client-docs/deploys/67d34f50984d820008951f1b
😎 Deploy Preview https://deploy-preview-12350--apollo-client-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link
Contributor

github-actions bot commented Feb 6, 2025

size-limit report 📦

Path Size
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client" (CJS) 42.24 KB (+0.18% 🔺)
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client" (production) (CJS) 37.73 KB (+0.23% 🔺)
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client" 32.68 KB (+0.19% 🔺)
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client" (production) 28.19 KB (+0.24% 🔺)
import { ApolloProvider } from "@apollo/client/react" 5.08 KB (0%)
import { ApolloProvider } from "@apollo/client/react" (production) 965 B (0%)
import { useQuery } from "@apollo/client/react" 7.19 KB (+0.17% 🔺)
import { useQuery } from "@apollo/client/react" (production) 3.07 KB (+0.29% 🔺)
import { useLazyQuery } from "@apollo/client/react" 6.3 KB (+0.19% 🔺)
import { useLazyQuery } from "@apollo/client/react" (production) 2.18 KB (+0.27% 🔺)
import { useMutation } from "@apollo/client/react" 6.37 KB (0%)
import { useMutation } from "@apollo/client/react" (production) 2.22 KB (0%)
import { useSubscription } from "@apollo/client/react" 6.77 KB (0%)
import { useSubscription } from "@apollo/client/react" (production) 2.63 KB (0%)
import { useSuspenseQuery } from "@apollo/client/react" 8.23 KB (0%)
import { useSuspenseQuery } from "@apollo/client/react" (production) 4.12 KB (-0.17% 🔽)
import { useBackgroundQuery } from "@apollo/client/react" 8.07 KB (0%)
import { useBackgroundQuery } from "@apollo/client/react" (production) 3.96 KB (0%)
import { useLoadableQuery } from "@apollo/client/react" 8.12 KB (0%)
import { useLoadableQuery } from "@apollo/client/react" (production) 4.02 KB (0%)
import { useReadQuery } from "@apollo/client/react" 5.74 KB (0%)
import { useReadQuery } from "@apollo/client/react" (production) 1.58 KB (0%)
import { useFragment } from "@apollo/client/react" 5.77 KB (0%)
import { useFragment } from "@apollo/client/react" (production) 1.63 KB (0%)

| {
// Defer to the passed in type to properly type the `@defer` fields.
data: T;
dataState: "hasNext";
Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not super in love with this name, but its been difficult to come up with something that doesn't sound the same as partial. The difference here is that partial is meant to describe a partial cache hit where some fields are missing, where hasNext is a @defer query that hasn't fully streamed in the full data set.

Other options considered:

  • streaming
  • pending

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe deferring ? otherwise streaming would work, but it's slightly harder to make the mental connection to @defer

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmmm ya this one is tough. Eventually this will need to work with @stream so I suppose streaming fits with that.

Copy link
Member

Choose a reason for hiding this comment

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

waitingForChunks maybe? Really not sure. streaming seems better than deferring to me, pending is often just used synonymous to loading, so I'd avoid that.

}
| {
// Defer to the passed in type to properly type the `@defer` fields.
data: T;
Copy link
Member Author

Choose a reason for hiding this comment

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

In the case of hasNext, I've left the T type alone. GraphQL Codegen will generate a type which includes a union on all deferred fields that handles the "missing" holes in the type.

The biggest downside to this is that the complete case also returns the type as-is which means the user will still have to check the deferred fields to ensure they are there to satisfy TypeScript. Ideally we could unwrap that type a bit, but this may prove to be challenging.

I welcome any thoughts here.

Copy link
Member

Choose a reason for hiding this comment

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

Let's wait how this shapes out on the codegen side. This seems fine for now.

// Even though `data` didn't change, the `dataStatus` is updated to reflect
// that the full result has been stremed in so we expect another render
// value.
await expect(stream).toEmitApolloQueryResult({
Copy link
Member Author

Choose a reason for hiding this comment

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

Now that we are reporting the dataStatus enum with a proper value, I think it is necessary to emit another value that reports the dataStatus as complete, even though the data property itself didn't change since the deferred fragment was masked.

@jerelmiller jerelmiller requested a review from phryneas February 6, 2025 22:28
@jerelmiller jerelmiller marked this pull request as ready for review February 6, 2025 22:29
@@ -734,9 +736,9 @@ export function toQueryResult<TData, TVariables extends OperationVariables>(
observable: ObservableQuery<TData, TVariables>,
client: ApolloClient<object>
): InternalQueryResult<TData, TVariables> {
const { data, partial, ...resultWithoutPartial } = result;
const { data, partial, dataState, ...resultWithoutPartial } = result;
Copy link
Member Author

Choose a reason for hiding this comment

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

We will expose this property in a separate PR so we can add support for it to all hooks.

Copy link
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.

Generally this looks good, but I'm a bit concerned that we have some code paths that will end up with a hasNext while others will end up with a partial for the same situation, depending if things come from a direct network request or from the cache while a network request is pending nonetheless.

It might even be a good idea to omit the hasNext case until we can guarantee consistency there, what are your thoughts?

Comment on lines 167 to 206
/**
* Describes the completeness of `data`.
* - `none`: No data could be fulfilled from the cache or the result is
* incomplete. `data` is `undefined`.
* - `partial`: Some data could be fulfilled from the cache but `data` is
* incomplete. This is only possible when `returnPartialData` is `true`.
* - `hasNext`: `data` is incomplete as a result of a deferred query and
* the result is still streaming in.
* - `complete`: `data` is a fully satisfied query result fulfilled
* either from the cache or network.
*/
Copy link
Member

Choose a reason for hiding this comment

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

Use @inheritDoc for the docblock so we have it on all four variants :)

}
| {
// Defer to the passed in type to properly type the `@defer` fields.
data: T;
Copy link
Member

Choose a reason for hiding this comment

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

Let's wait how this shapes out on the codegen side. This seems fine for now.

| {
// Defer to the passed in type to properly type the `@defer` fields.
data: T;
dataState: "hasNext";
Copy link
Member

Choose a reason for hiding this comment

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

waitingForChunks maybe? Really not sure. streaming seems better than deferring to me, pending is often just used synonymous to loading, so I'd avoid that.

@@ -823,15 +823,16 @@ export class QueryManager<TStore> {
return this.fetchQuery<TData, TVars>(queryId, { ...options, query })
.then(
(result) =>
result && {
result &&
Copy link
Member

Choose a reason for hiding this comment

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

Is this truthyness test still necessary?

data: data as TData | undefined,
const result = {
data,
dataState:
Copy link
Member

Choose a reason for hiding this comment

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

This means that streaming (or whatever we'll call it) will be kinda non-deterministic, depending on if this ObservableQuery triggered the query or if it's currently being read & updated from the cache.
Should we add a streaming status at all given that context or wait until we can get this 100% accurate?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is part of the reason I wanted to add a source property at some point because I think this becomes more obvious what that state. streaming will only ever be emitted from the link observable and that code path doesn't go through fromData/resultsFromCache. This observable value is always the first one emitted, so the link observable will ensure that the proper dataState is set from that point forward.

That said, perhaps I'm misunderstand what you're referring to as non-deterministic?

@jerelmiller
Copy link
Member Author

I got this rebased, though I think I want to think this through a bit more before we merge it. I think there is more opportunity for optimization here. For example, client.query(...) should never have a streaming or partial result, so we are unnecessarily forcing end users to perform a check against dataState before that data is usable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants