-
-
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(rtk-query/react): add useUnstable_SuspenseQuery
hook
#2149
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 f2d0ec6:
|
✅ Deploy Preview for redux-starter-kit-docs ready!
To edit notification comments on pull requests, go to your Netlify site settings. |
useUnstable_SuspenseQuery
hook
) | ||
|
||
if (pendingPromise) { | ||
throw pendingPromise |
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.
If I'm reading this correctly, this would work like this right now:
- render the component once (loading true, no promise)
- fire the effect to make a request
- render the component again
- throw into suspense
Could we also achieve that without the initial render? The initial render kind of destroys the promise of "data will always be available, you don't need to check for a isLoading
flag".
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 said, I don't want to downplay the work you are putting into this - I'm very happy to see you picking this up!)
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.
If I'm reading this correctly, this would work like this right now:
* render the component once (loading true, no promise) * fire the effect to make a request * render the component again * throw into suspense
Could we also achieve that without the initial render? The initial render kind of destroys the promise of "data will always be available, you don't need to check for a
isLoading
flag".
You're right; thank you for your feedback 👍
Triggering a prefetch solves this issue:
+ dispatch(api.util.prefetch(name as any, arg as any, { ifOlderThan: false }))
+
const pendingPromise = api.util.getRunningOperationPromise<any>(
name,
arg as unknown as string
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.
fixed in a6b7f36
An interesting comment from Reactiflux:
In other words, the hooks start the request immediately, and it's the I think I can sorta see what's being said here. Something to consider. |
I do think its worth clarifying that the reactiflux example with // render-then-you-fetch
// will never trigger suspense
// parent component
// data is null until the fetch is complete
const { data } = useLazyQuery(...)
data && <ChildComponent data={data} />
// child component
const { data } = useFragment(..., props.data) it is true that in // render-as-you-fetch
// parent component
// doesn't trigger suspense
const [preloadedQuery] = useQueryLoader(...)
// can kick off fetches in parallel
// const [preloadedQuery2] = useQueryLoader(...)
// const [preloadedQuery3] = useQueryLoader(...)
<ChildComponent preloadedQuery={preloadedQuery} />
// child component
// this will trigger a suspense boundary until the query is finished
const { data } = usePreloadedQuery(..., props.preloadedQuery); another interesting pattern i've seen is with const [data1, data2] = useRecoilValue(waitForAll([dataSource1, dataSource2])) another interesting approach is this
i would argue that this is the pattern for using data fetching with suspense. both preloading or some sort of |
getRandomIntervalValue() | ||
) | ||
|
||
const { data, isFetching, refetch } = |
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.
is there a way to type isFetching
as void or remove it from the return value if suspendOnRefetch
is true?
if suspendOnRefetch
is true
, isFetching
is always going to be false
when accessed in user code
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.
is there a way to type
isFetching
as void or remove it from the return value ifsuspendOnRefetch
is true?if
suspendOnRefetch
istrue
,isFetching
is always going to befalse
when accessed in user code
You can play with ts
types in order to have isFetching
be always true if suspendOnRefetch
is set to true
but,
no I don't think that we should remove isFetching
from the response.
} | ||
} else if ( | ||
queryStateResults.isError && | ||
!queryStateResults.isFetching |
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 this is interesting behavior. Not sure how everyone would expect this to behave.
i would think that if there is an error, it will always throw an error. and once the user resets the error boundary and thus rerenders the component that had the suspended query that threw the error, it will refetch again
-- Description Adds useUnstable_SuspenseQuery, an alternative to `useQuery` that can be used to render-as-you-fetch. -- References - https://reactjs.org/docs/concurrent-mode-suspense.html - https://github.com/facebook/react/blob/12adaffef7105e2714f82651ea51936c563fe15c/packages/react/src/ReactLazy.js -- Alternatives - swr: https://swr.vercel.app/docs/suspense - react-query: https://react-query.tanstack.com/guides/suspense
0395df8
to
f2d0ec6
Compare
Thank you @markerikson @wuweiweiwu for your suggestions and sorry for the delayed reply: I generally don't do open source development during working days.
It breaks down when you need to query things in parallel I agree. Suggested alternatives1. result.read()
This pattern is a pandora's box of potential misuses: 1.a Accidental waterfallsconst { readThing1 } = useSuspendedThing1();
const thing1 = readThing1(); // oops waterfall!
const { readThing2 } = useSuspendedThing2();
const thing2= readThing2(); 1.b Accidental exception thrown during effectThis pattern is really problematic if there's a closure on the accessor function ( const { readThing1 } = useSuspendedThing1();
useEffect(() => {
if(checkConditionOn(prop1, prop2)) {
setValue(readThing1() ) // It could throw a promise or an error here
}
}, [prop1, prop2]); 2 SWR RFC vercel/swr#168I deem this approach hackish because it breaks the rule of hooks. Given your feedback I'm not sure that it makes sense to keep on going with this design and I'd like to try to tackle the problem with a different approach. useSuspendQueries
Usageconst [{ data: query1Res }, { data: query2Res }] = useSuspendQueries(
useQuery1(arg1),
useQuery2(arg2),
); I'll attempt this approach in a new PR. |
Just as a small api suggestion, maybe more something in the direction const [{ data: query1Res }, { data: query2Res }] = useSuspendQueries(
// no hooks "passed in elsewhere" - that can easily break rules of hooks in unintended ways
[endpoint1, arg1],
[endpoint2, arg2],
); Although, tbh, I'm not really sure about that approach.
I honestly think it is okay - many components just query one api and everything we do here could only prevent per-component waterfalls. So it might make sense to just put more focus on prefetching. The simple use case (one fetch in the component, we do not care too much about cross-component waterfalls) could just be const [data, { refetch }] = useSuspendedThing1(arg1) While prevening an in-component-waterfall would look like function MyComponent(){
usePrefetchThing1(arg1)
usePrefetchThing2(arg2)
const [data1, { refetch }] = useSuspendedThing1(arg1)
const [data2, { refetch }] = useSuspendedThing2(arg2)
return <>...</>
} or, if something like // in router:
ensureThingFetching1(arg1)
ensureThingFetching2(arg2)
// in component
function MyComponent(){
const [data1, { refetch }] = useSuspendedThing1(arg1)
const [data2, { refetch }] = useSuspendedThing2(arg2)
} |
Asked on Twitter: https://twitter.com/acemarke/status/1507740922804637707 and got some responses. @drcmda pointed out https://github.com/pmndrs/suspend-react and https://github.com/pmndrs/jotai/blob/main/docs/basics/async.mdx, which might be useful API examples |
agreed. To achieve true
those apis seems to be inline with what |
I do not get why The solution I've proposed does not break the rule of hooks because my idea is to add a simple It is a simple contract that would allow to suspend non rtk-query resources.
I'm not sure about this: In my experience it is rather common to have a component that relies on at least 2 distinct data sources.
I see those prefetch calls as avoidable boilerplate code: Conclusion@phryneas has suggested a reasonable alternative to the version of I'll tinker a bit with these 2 api signatures and then I'll weight pros and cons of their implementations. |
Ah sorry, I had misread your example! I thought you were passing the hook in, not the hook result. In the end, just keep exploring - in the end I'm just watching from the sidelines here at the moment :) |
Just curious, but "unstable" ? Suspense has been part of React since 16.6, no difference between React.lazy and useQuery. React 18 intents to ship, they're just fixing docs. |
@drcmda suspense for data fetching is not part of the initial React 18 release and "how it should be used" has still not be fully fleshed out by the React team. It would be foolish to go with an api now that we might have to scrap later because they invent some pattern we don't know about yet. |
imo suspense for lazy loading and "suspense for data fetching" are one and the same, you throw a promise. even the truly experimental parts like |
@drcmda As I interpret the statements of the React team, suspense for lazy loading is a subset of what suspense for data fetching is supposed to be - but afaik the public just doesn't know all the details for it yet. |
suspense is a thrown promise, nothing more. react just re-renders when it's resolved. in order to not cause a loop you cache. they may or may not give us tools to handle that part in the future. either way, https://reactjs.org/blog/2022/03/29/react-v18.html#suspense-in-data-frameworks
with next using suspense in prod nothing's going to change how it fundamentally works. :-) |
UpdateI'm making good progress with the api I've proposed in #2149 (comment) I hope to have it ready next weekend. |
Just want to give a huge "+1" to @FaberVitale on this one. I implemented this as an over the top solution for my project and it works great!! The only problem is that due to the type generation used by this library it's all but impossible to generalize this so that I don't have to write a wrapper for each method i want to suspense (which is all of them actually). Here is a snippet of the code: export const createSuspendedQueries = <
A extends BaseQueryFn<any, unknown, unknown, {}, {}>,
B extends EndpointDefinitions,
C extends string,
D extends string
>(api: Api<A, B, C, D>) => {
return Object
.keys(api.endpoints)
.filter((e) : e is any => !!((e as any).useQuery))
.map((name) => {
const endpoint = api.endpoints[name];
const a : typeof endpoint["useQuery"] = (args) => {
const endpoint = api.endpoints[name];
const dispatch = useDispatch()
const queryOutput = (endpoint as any).useQuery(args);
const readData = () => {
if (queryOutput.error) {
throw queryOutput.error;
}
if (!queryOutput.isFetching && queryOutput.data) {
return queryOutput.data
}
let promise = queryQuestionBank.util.getRunningOperationPromise(name, args)
if (promise) {
throw promise.unwrap()
}
dispatch(
queryQuestionBank.util.prefetch("contentTree", args, { force: true })
)
promise = queryQuestionBank.util.getRunningOperationPromise("contentTree", args)
if (promise) {
throw promise.unwrap()
}
throw new Error("we goofed")
};
return {
...queryOutput,
readData
}
}
})
} this works all right as far as JS goes, the TS types are all screwed up though. Can't wait for this to make it to master: it's a game changer as far as writing next js apps goes 😄 |
I've just uploaded a second POC that is based on #2149 (comment) Thank you for your feedback :) |
Description
Adds
useUnstable_SuspenseQuery
, an alternative touseQuery
that can be used to trigger<Suspense />
while fetching data.Accepts the same arguments of
useQuery
and adds the following options:Returns
useQuery
output withoutisLoading
(useless) andstatus
(deprecated).Demo
Other
render-as-you-fetch
implementationsTodo
References
Please provide feedback and suggestions 🙏