-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
useSuspenseQuery
#10323
useSuspenseQuery
#10323
Conversation
I'm getting some test failures in this branch for unrelated tests. I rebased with |
After some investigation for the test flake on this issue, my guess is this comes down to timers. In this test, we are checking that polling works as expected. Our expectations use I wanted to at least document my findings. I have been able to run the whole test suite successfully, though its very intermittent. Hopefully I can get a run that passes so we can merge this PR when reviews are finished. |
ccbe0ad
to
4a0bbb8
Compare
42a078e
to
37ec144
Compare
🦋 Changeset detectedLatest commit: 5212938 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
0936050
to
8211fa6
Compare
37ec144
to
7558c2a
Compare
b863dc1
to
7755708
Compare
f65b6c5
to
f4e0ece
Compare
// Due to the way the suspense hook works, we don't subscribe to the observable | ||
// until after we have suspended. Once an observable is subscribed, it calls | ||
// `reobserve` which has the potential to kick off a network request. We want | ||
// to ensure we don't accidentally kick off the network request more than | ||
// necessary after a component has been suspended. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment is now out-of-date with the introduction of the fetchOnFirstSubscribe
option. I'll amend this comment to be more accurate with the change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super excited for this! 🚀🎉🙌
Does this PR take advantage of the new "use" proposal currently in alpha from the React team? Also, I notice you are using a cache. I understand that currently, that is one of the pieces missing from the React 229 PR. That is, without having a consistent cache, Suspense has problem with any data persistence between client and server. Is this PR strictly meant for client only testing? |
@pkellner no it does not. We will likely migrate to it when it's ready and stable, but for now we're aiming to get this in front of as many devs using Apollo Client as we can. We are keeping on eye on that RFC to see how things evolve and if it will make sense for us to migrate to
For now, yes! As mentioned in the PR description:
This may work on the server with React 18, but we do not yet have any kind of cache hydration mechanism for it, so some of the advantages you get with SSR are moot. As mentioned in the "Release strategy" section of the RFC, formal SSR support (with cache hydration) will be coming in a future alpha. I suspect the implementation will evolve quite a bit between this PR and our GA release (sometime early 2023) as we learn and get more feedback on our Suspense implementation. In the mean time, if you'd like to try out at least the base-level implementation, I'd welcome any and all feedback you have! I saw you commented quite a bit on the original issue so I know you've been thinking about this a lot already. |
Thank you for the ongoing work with this feature. I believe this implementation suffers from the waterfall issue I pointed out in this comment #10231 (comment). Is it possible to dispatch 2 queries within the same suspense boundary and have them run at the same time? |
Hey @laverdet! You're correct in that this implementation does suffer from the waterfall issue that you mentioned. After some thought and discussion, I purposefully made it this way. I updated the RFC a while back to explain this behavior. Check out the "Notable changes in behavior from Check out my comment that explains why I decided to go this route. Specifically the observation I made about the new
In my research on other libraries' implementations, it seems this was the common pattern: suspend immediately when the data is pending. To keep from being too "magic", I figured this was the best way forward and recommend splitting the queries between 2 components. As an FYI, we do plan on enabling the render-as-you-fetch pattern with We'll have more info on |
Sorry for not responding in more detail to the RFC comment. I'm not sure if you found it, but there's actually a lot more context in the React RFC PR: reactjs/rfcs#229 Dan Abramov had this example code in his reply here: const promise1 = fetchData1()
const promise2 = fetchData2()
// these are already parallel by now!
const data1 = use(promise1)
const data2 = use(promise2) This is briefly touched on in the RFC under: "Caveat: Data requests must be cached between replays". The idea is that suspense-capable loaders would return referentially-stable thenables. A transitional (pre- interface WithSuspense<Type> extends PromiseLike<Type> {
get(): Type;
}
function makeSuspense<Type>(promise: Promise<Type>): WithSuspense<Type> {
let resolved = false;
let rejected = false;
let result: unknown;
promise.then(value => {
resolved = true;
result = value;
}, error => {
rejected = true;
result = error;
});
return {
get() {
if (resolved) {
return result as Type;
} else if (rejected) {
throw result;
} else {
throw promise;
}
},
then(onfulfilled, onrejected) {
return promise.then(onfulfilled, onrejected);
},
};
} With this implementation we'd be able to do something like: const result1 = useSuspenseQuery(...);
const result2 = useSuspenseQuery(...);
const data1 = result1.data.get();
const data2 = result2.data.get(); Then after (if) const result1 = useSuspenseQuery(...);
const result2 = useSuspenseQuery(...);
const data1 = use(result1.data);
const data2 = use(result2.data); |
@laverdet ah I see what you're getting at. I'd like to better understand the use case for multiple queries in a single component to think about this some more. While I know some developers do this, I'm wondering what this enables vs splitting up queries into multiple components. FWIW, I see
When we implement it, I see Would this approach/pattern address the solution you're looking for? To be transparent, I see the approach of returning the data directly (no unwrapping) as a bit simpler since you can just use it right away with no additional lines of code to unwrap the value. I chose that deliberately as it seems to be the common pattern around many suspense-enabled hooks in the wild. I'm also banking on the fact that it's likely much rarer that you use multiple queries in a single component, hence why I chose the simpler approach. I'm banking on the vision of I definitely hope you provide feedback as we get more of these alphas out! Once we get more of the full Suspense vision completed, I think we'll be able to enable a lot more powerful patterns. |
Idk, it happens sometimes. Maybe this is a component that thought some data would be preloaded but actually it wasn't and now it needs to suspend. I don't think the code noise is too bad with an explicit const { data } = useQuery(...);
const myObject = data?.myQuery?.myObject; In this case it would be: const { data } = useSuspenseQuery(...);
const myObject = data.get()?.myQuery?.myObject; The biggest problem I have with this implementation is that we can't put the suspense toothpaste back into the tube once you've already squeezed it out for us. If developers are truly unable to burden the cognitive load of In the (explicitly outdated, but yet to be replaced) React Suspense docs they apply a similar pattern: const resource = fetchProfileData();
// [...]
function ProfileDetails() {
// Try to read user info, although it might not have loaded yet
const user = resource.user.read();
return <h1>{user.name}</h1>;
} |
Hey @laverdet 👋 ! Sorry for my slow reply. Apparently I forgot to actually submit the comment I had typed up reply to you 😬
I totally understand your point here. I agree with your point about wanting to parallelize as much as possible. I'll just reiterate that I believe I'd love to revisit this topic once we have a more full picture of the suspense story in Apollo and get your feedback at that point.
I hear you and agree that its not a lot to learn or read. I'll offer this point: Something that I've always loved about Apollo's approach from the very first time I used it is it always felt like "plain JavaScript". A while ago I had a chance to work on a greenfield project for the company I was with at the time and we had a chance to pick our tech stack. We had settled on GraphQL and it came time to choose what GraphQL client we wanted to use. Apollo and Relay were the 2 big offerings at that time. We had initially decided to use Relay, but after a time, it got a bit difficult to onboard new people with all the concepts that had to be learned to use it. Relay used a lot of opaque objects that you had to understand or pass around. After some time, I took a 2nd look at Apollo. Using Apollo, I really appreciated that I was working with plain objects and didn't have to understand opaque types to get the best out of the client. Sure, Hope that helps! Again, I sincerely appreciate the feedback. |
I gave const { query, variables } = userId ? { query: GetRelationship, variables: { userId } } : {};
const result = useSuspenseQuery(query, { variables });` |
Would you be able to give me some pseudo code to sketch out what you mean here? To use Relay as an example and the best parallel right now, I see
This is really good to know! Would you be willing to open a separate issue for this as a request for the hook? I'd like to be able to track this separately. One of the goals in this initial PR for the hook was to limit the API to test what we actually need in there. Since To be honest, I've been working on a demo on the side to get a feel for Thanks for the feedback! |
// Prime the cache for the upcoming component
useBackgroundQuery(query1);
useBackgroundQuery(query2);
// [...] a bunch of other code, maybe even in another component
const data = useSuspenseQuery(query1);
// we don't need this anymore because the product specification changed
// const otherData = useSuspenseQuery(query2); I've never used Relay but I understand a big part of the framework is a compiler which lifts query fragments out of child components to combine them into dispatchable queries in a top component. This ensures that each and every field in a query is used by some component. I don't know how
You can do it with conditional types: type MaybeTypedDocumentNode<Data, Variables> = TypedDocumentNode<Data, Variables> | null;
type ResultForMaybeTypedDocumentNode<Type> =
Type extends TypedDocumentNode<infer Data, infer Variables>
? QueryResult<Data, Variables>
: null;
export function useSuspenseQuery<Query extends MaybeTypedDocumentNode<Data, Variables>, Data, Variables>(
query: Query,
unstableVariables: Variables,
options?: QueryOptions,
): ResultForMaybeTypedDocumentNode<Query> { If you wanted to maintain the |
Relates to #10231 and #10286
Adds a new
useSuspenseQuery
hook to allow React apps to use Apollo with React's suspense feature. For now, this hook is exported as an experimental hook to make it clear that we are looking for feedback and iterating on it.This implementation in this PR focuses on baseline support for React suspense with Apollo. This does not yet support
@defer
directives, has not been tested with SSR, and does not provide a solution for the render-as-you-fetch pattern . These will be covered in future PRs and alphas.Usage
<ApolloProvider />
has aSuspenseCache
Design
useSuspenseQuery
has been modeled afteruseQuery
to make the hook feel familiar to those that use Apollo, though it is a much more scaled down version. I did not add support for all options nor return the same set of values you get from theuseQuery
hook. My goal is to start with a small surface area and add options/return values as we get feedback through our alpha release process.Supported options
variables
client
errorPolicy
context
returnPartialData
canonizeResults
fetchPolicy
(all butcache-only
andstandby
. See the RFC for more info)nextFetchPolicy
refetchWritePolicy
Returned properties
data
error
refetch
fetchMore
Checklist: