- 
          
- 
                Notifications
    You must be signed in to change notification settings 
- Fork 1.2k
feat: add useSuspendAll hook & react/suspense example #2245
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
base: master
Are you sure you want to change the base?
Conversation
| This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 65ba8a3: 
 | 
| if (!pendingPromise) { | ||
| throw new Error( | ||
| `[rtk-query][react]: invalid state error, expected getRunningOperationPromise(${name}, ${queryStateResults.requestId}) to be defined` | ||
| ) | ||
| } | 
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.
Do you see this ever happening? Also, are we saving another promise than prefetch returns into the pending promises? Otherwise we could reuse that.
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.
Do you see this ever happening?
No , it's an invariant that was useful while iterating on this feature. I can remove it.
Also, are we saving another promise than prefetch returns into the pending promises? Otherwise we could reuse that.
usePrefetch() returns a consumer:
const prefetch: (arg: any, options?: PrefetchOptions | undefined) => voidI do not think that I can extract a promise from that.
I've skimmed a bit the type of api.util.prefetch and it seems that has type:
<EndpointName extends QueryKeys<Definitions>>(
      endpointName: EndpointName,
      arg: any,
      options: PrefetchOptions
    ): ThunkAction<void, any, any, AnyAction>How can I extract a promise from that thunk action, I've not found a test that unwraps a promise from api.util.prefetch.
Can you please provide more details?
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.
We would need a return dispatch  here everywhere this has only a dispatch to get that promise reliably: 
redux-toolkit/packages/toolkit/src/query/core/buildThunks.ts
Lines 453 to 468 in e7a98eb
| dispatch(queryAction()) | |
| } else if (maxAge) { | |
| const lastFulfilledTs = latestStateValue?.fulfilledTimeStamp | |
| if (!lastFulfilledTs) { | |
| dispatch(queryAction()) | |
| return | |
| } | |
| const shouldRetrigger = | |
| (Number(new Date()) - Number(new Date(lastFulfilledTs))) / 1000 >= | |
| maxAge | |
| if (shouldRetrigger) { | |
| dispatch(queryAction()) | |
| } | |
| } else { | |
| // If prefetching with no options, just let it try | |
| dispatch(queryAction(false)) | 
Perfectly viable to just add that.
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.
MEMO: It's a breaking 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.
returning something from a function that returned undefined before? I wouldn't really call that breaking
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.
prefetch thunk and usePrefetch callback now return an object.
interface PrefetchActionCreatorResult {
  /**
   * Returns a promise that **always** resolves to `undefined`
   * when there are no pending requests initiated by input thunk.
   */
  unwrap(): Promise<void>,
  /**
   * Cancels pending requests.
   */
  abort(): void
}| name | ||
| ) | ||
|  | ||
| const prefetch = usePrefetch(name as any) | 
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.
using prefetch here might be problematic since that creates a value subscription that cannot be unsubscribed. if we use that here we have to look into that
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.
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.
I think at this point we probably need to overwork prefetch so that it takes something like a "keepFor" timer to add a time-based temporary subscription (maybe with a fixed subscription id of "prefetch" so that multiple prefetches just "bump up the timer").
I hate to bother you to go deeper into this rabbit hole - but could you look into this?
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.
Done f994418 :)
Please let me know what you think
I'm still not sure about the suggested changes to prefetch return type  but I'll look into it.
| .filter(isPromiseLike) as Promise<unknown>[] | ||
|  | ||
| if (promises.length) { | ||
| throw Promise.all(promises) | 
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.
That would create a new promise each time, but we need to keep it stable. Maybe just throw the first unresolved promise? (We would need to add an isResolved property to the promises in that case)
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.
That would create a new promise each time, but we need to keep it stable. Maybe just throw the first unresolved promise? (We would need to add an
isResolvedproperty to the promises in that case)
I've not seen suspense for data fetching implementation that relies on isResolved or similar.
That would create a new promise each time
Yes but it is not a problem because you are suspending the rendering until all the promises are resolved and
runningQueries does not keep promises that are settled.
Moreover it is the same approach used by joitai waitForAll
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.
I remember reading in the working group that for suspense it is important to always get the same promise back, but right now I can't find the quote for it.
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.
Yep, pretty sure I saw that too, but don't immediately have a pointer either.
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.
Yep, pretty sure I saw that too, but don't immediately have a pointer either.
I'll look into it 👍 .
I've got a couple of ideas on how to tackle this issue but I'd like to migrate the examples to react 18 first.
| This looks like a great approach, but with  I have skimmed the source and left some comments, but I have spent all day recording egghead video lessons, so my brain is mush. Very likely that some comments don't make sense or I missed some things. But please keep going! 👍 | 
7ef04e5    to
    3c5d662      
    Compare
  
    800a54a    to
    9a8f729      
    Compare
  
    9a8f729    to
    36f52db      
    Compare
  
    …mer reduxjs#1283 Closes reduxjs#1283 Automatically removes prefetch subscriptions after configurable amount of time. Description: Prefetch subscription are now removed after `prefetchOptions.keepSubscriptionFor` if provided or api.config.keepPrefetchSubscriptionsFor otherwise. Api changes: - adds `keepSubscriptionFor` to prefetchOptions - adds `keepPrefetchSubscriptionsFor` to api.config (default 10s) Internal changes: - prefetch queries now have the same requestId and the same subscription key
Description:
prefetch thunk now returns an object with shape:
```ts
export interface PrefetchActionCreatorResult {
  /**
   * Returns a promise that **always** resolves to `undefined`
   * when there are no pending requests initiated by input thunk.
   */
  unwrap(): Promise<void>,
  /**
   * Cancels pending requests.
   */
  abort(): void
}
```
    36f52db    to
    1f17ca7      
    Compare
  
    | ✅ Deploy Preview for redux-starter-kit-docs ready!
 To edit notification comments on pull requests, go to your Netlify site settings. | 
| unwrap() { | ||
| return initiateOutput.unwrap().then(noop, noop) | ||
| }, | ||
| abort: initiateOutput.abort, | 
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.
what about unsubscribe? Why not just return initiateOutput as is?
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.
initiateOutput returns QueryActionCreatorResult
type QueryActionCreatorResult<
  D extends QueryDefinition<any, any, any, any>
> = Promise<QueryResultSelectorResult<D>> & {
  arg: QueryArgFrom<D>
  requestId: string
  subscriptionOptions: SubscriptionOptions | undefined
  abort(): void
  unwrap(): Promise<ResultTypeFrom<D>>
  unsubscribe(): void
  refetch(): void
  updateSubscriptionOptions(options: SubscriptionOptions): void
  queryCacheKey: string
}It returns methods that do not fit the prefetch model like updateSubscriptionOptions and can cause more harm than anything else if misused.
I just want to expose the informations strictly useful.
Moreover we can always expose later more properties of initiate output if necessary, speaking of which we should at least expose arg and queryCacheKey.
|  | ||
| return { | ||
| unwrap() { | ||
| return initiateOutput.unwrap().then(noop, noop) | 
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.
looks like this way you remove information about both fulfilled and rejected values
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.
Yeah it makes sense to at least return the output of unwrap
| Also,  | 
| 
 I'm afraid I do not agree with this change. The whole point of prefetch is to put data in the store because you anticipate that you are going to query later. Moreover there's already  | 
| Prefetch PRI'd like to move the prefetch changes (see #1283) in another PR. | 
…o feat/use-suspend-all
| Sorry, I didn't realize  | 
| FYI, the React team just put up a PR that looks to add what might eventually be "official" support for Suspense triggering:  It still throws internally, but only after introspecting the promise: the PR says throwing directly would eventually be deprecated | 
| 
 Caching promises
 A shallow equalilty check of the pending promises list should be enough to avoid issues during updates. | 
| Kevin Ghadyani put up a vid with him talking to Tanner about how React Query does Suspense. Haven't even watched it yet, but throwing it here for reference: | 

Description
Proof of concept of a suspense for data fetching implementation.
It is based on
useSuspendAll, a custom hook that parallelizes suspended queries and outputsinput arguments with remapped TS types.
Demo
Issues
Closes #1283 #1574
Usage
1. Simple single query
2. Skipped Query
3. Parallel queries
4. Error Handling
Technical details
useSuspendAllexpects one or more objects that implement theSuspendableinterface as arguments:useSuspendAllreturns the input arguments in array and does not mutate them, it uses instead TS property remappingchange TS output if input arg extends
SuspendableResource:Changes to existing APIs.
Adds the following properties to
api.endpoints.useQueryoutput:getSuspendablePromise
isSkipped:
trueif query is skippedadds
keepSubscriptionForto prefetchOptionsadds
keepPrefetchSubscriptionsForto api.config (default 10s)prefetch subscriptions are now removed after
prefetchOptions.keepSubscriptionFor, if provided,or
api.config.keepPrefetchSubscriptionsForotherwise.Prefetch Thunk and
usePrefetchpreviously returned undefined now returnPrefetchActionCreatorResult:Internal changes
New Apis
Todo
err instanceof SuspenseQueryErrordoesn't workAlternatives
Similar apis
Previous attempts
useUnstable_SuspenseQueryhook #2149