-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
feat: add useSuspendAll hook & react/suspense example #2245
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) => void
I 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
isResolved
property 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
useSuspendAll
expects one or more objects that implement theSuspendable
interface as arguments:useSuspendAll
returns 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.useQuery
output:getSuspendablePromise
isSkipped:
true
if query is skippedadds
keepSubscriptionFor
to prefetchOptionsadds
keepPrefetchSubscriptionsFor
to api.config (default 10s)prefetch subscriptions are now removed after
prefetchOptions.keepSubscriptionFor
, if provided,or
api.config.keepPrefetchSubscriptionsFor
otherwise.Prefetch Thunk and
usePrefetch
previously returned undefined now returnPrefetchActionCreatorResult
:Internal changes
New Apis
Todo
err instanceof SuspenseQueryError
doesn't workAlternatives
Similar apis
Previous attempts
useUnstable_SuspenseQuery
hook #2149