swr
swr copied to clipboard
[RFC] useSWRSuspense (or Suspense guards)
With useSWRSuspense
you can avoid waterfall in suspense mode:
function App () {
const [user, movies] = useSWRSuspense(()=> {
const { data: user } = useSWR('/api/user')
const { data: movies } = useSWR('/api/movies')
return [user, movies]
})
// or
// const [user, movies] = useSWRSuspense(swr => [swr('/api/user'), swr('/api/movies')])
return (
<div>
Hi {user.name}, we have {movies.length} movies on the list.
</div>
)
}
Note that swr
function is the exact same as useSWR
, it's for avoiding linter errors
It will maximize the parallelism while under suspense mode. In the example above both APIs will start to fetch at the same time, and the actual render will be suspended until both are loaded.
Another proposal
Another way is to use 2 "Suspense guard" hooks:
function App () {
useSWRSuspenseStart()
const { data: user } = useSWR('/api/user')
const { data: movies } = useSWR('/api/movies')
useSWRSuspenseEnd()
return (
<div>
Hi {user.name}, we have {movies.length} movies on the list.
</div>
)
}
Which does the same thing (but it might be easier for users to make mistakes).
Checklist
- [ ] make sure it works in concurrent mode
- [ ] better error handling
- [ ] error if the callback throws a promise (misuse of
suspense: true
insideuseSWRSuspense
) - [ ] documentation
Fixes #5.
Thank you for prioritizing this feature ❤️
In my head, the API should look something like below
function MyComponent(props) {
// First we declare the list of fetch calls. Here we use an object. Could be an array instead.
// Doing this will NOT trigger suspense. i.e. A promise shall not be thrown.
// But the API calls will be triggered in parallel
const fetcher = useSuspenseSwr({
key1: {/*fetch config for key 1*/},
key2: {/*fetch config for key 2*/},
});
// and then call fetcher.fetch('key1') somewhere in the code
// This is when the promise will be thrown
}
This way, the API calls are triggered immediately, and parts of the component which are not dependent on the API calls can render.
The problem I see in your API is that the fetch calls are completely blocking the render. It does avoid the waterfalls, but it does not give the full power of suspense to the users.
Thank you again for the amazing features and support!
If possible, I would write the API's like this:
export function App() {
const [user, movies] = useSWR(
['/api/user', '/api/movies'], { suspense: true }
);
// or const { user, movies } = useSWR(['/api/user', '/api/movies'], { suspense: true });
// or const [user, movies] = useSWR(swr => [swr('/api/user'), swr('/api/movies')], { suspense: true });
return (
<Suspense fallback={<Spinner />}>
<div>
Hi {user.name}, we have {movies.length} movies on the list.
</div>
</Suspense>
);
}
Personally, I would prefer keeping the hook naming "clean" (sticking with just useSWR
) by sticking with this commonly seen format ->
fetch(url: string, options: Object)
It would seem to make more sense to keep the suspense option in an options object, but I don't know what the consequences would be to the API. For example, what would the functionality be if a user were to have two fetches, but was missing the Suspense option? Do we just fetch two things before rendering like normal?
@samsisle what you proposed is somehow possible right now:
function fetcher(...urls) {
return Promise.all(url.map(url => fetch(url).then(res => res.json())));
}
const { data } = useSWR(["/api/user", "/api/movies"], fetcher)
const [user, movie] = data;
Something like that, your fetcher could be technically anything and don't necessarily needs to do a single request so you could read your array of URLs and run multiples fetches.
Note this will mean if you change something in one of those URL (e.g. a param or query) all of them will be fetched again. The proposal useSWRSuspense
should avoid that from what I saw, since it will let you still have multiple useSWR
calls, one per URL.
@quietshu the second option is not so easy to understand, it's probably simple but I think I can get a sense on how the first one works just by seen the example, but with the second one I'm not sure.
@sergiodxa for the second example, it works like this under the hood:
useSWRSuspenseStart() // promises = []
const { data: user } = useSWR('/api/user') // promises.push(fetch('/api/user'))
const { data: movies } = useSWR('/api/movies') // promises.push(fetch('/api/movies'))
useSWRSuspenseEnd() // throw Promise.race(promises)
The reason I'm not using
const [user, movies] = useSWR(
['/api/user', '/api/movies'], { suspense: true }
);
is because it loses the ability to do dependent fetching, meaning I can't do this in suspense mode:
(source)
But a function will still have the same expressiveness and parallelism (+ ability to be suspended by the boundary):
const [user, movies] = useSWRSuspense(()=> {
const { data: user } = useSWR('/api/user')
const { data: posts } = useSWR('/api/posts')
const { data: movies } = useSWR(() => '/api/movies?id=' + user.id)
return [user, posts, movies]
})
Couldn't this be done just by splitting things up?
- If requests are not dependent on each other, they can be done in separate components.
- If they are dependent, they can't be done in parallell anyways, and need to be done sequentially, which is how multiple
useSWR( , { suspense: true })
works now, I believe?
@Svish I agree that ideally you should split your components and call it a day, however sometimes it's not possible to split things up.
I'm using a beta version of SWR with this feature because in a component I have multiple requests that are needed to render the UI, but they don't depend on each other, so they could totally be run simultaneously, without this feature I was causing an unnecessary waterfall of requests.
Can we ressurect this? I'm using swr for react-three-fiber (essentially vr application). In that ecosystem, suspense is heavily used, but waterfalling requests is a pretty much a show stopper for me. I'd be happy to help with testing/contributing
So a feedback on the proposals: each of them has a certain flaw (well, they are already listed in the starting comment, but I think elaborating won't hurt)
-
useSWRSuspense
with a callback that callsuseSWR
- looks the best, flexible (existing hooks that wrapuseSWR
can be reused), but it's gonna produce a linter warning about a rules of hooks every time -
useSWRSuspense
with a callback that usesswr
- no linter error, but no reuse of existing wrappeduseSWR
hooks usage (or a refactoring work to allow them to accept it from the outside) -
useSWRSuspenceStart/End
- Look kinda clunky, but the hooks can be reused and no linter errors
If you ask me, I would go for a third one, I favor flexibility and reusability and rely on linters heavily
@sergiodxa you said that you're using a beta that has this? Can you point where I can find this beta version if it still exists?
Thanks for the feedback @saitonakamura! Personally I like the third one as well, but I'd wait until Suspense & react-fetch (official data fetching lib with Suspense) get finalized first and then continue with this feature.
@saitonakamura I'm not using it anymore, the beta version for this is really old too, way before v1 of SWR and I don't remember the version to use in the package.json
and I can't check in the project repository anymore.
The issues you mentioned are things I saw, linter complaining all the time, but I was able to use it custom hooks calling SWR internally
@shuding waiting for suspense finalization makes sense (especially in vanilla react world). Although, if you're afraid of people adopting it, I guess it can be released under unstable_
prefix like react team does
What about this API?
function App() {
// Start fetching the user data without suspending.
const userResource = useSWRSuspense('/api/user')
// Start fetching the movies data without suspending.
const moviesResource = useSWRSuspense('/api/movies')
// Throws the actual promise, suspends until the user data has loaded.
const { data: user } = userResource.read()
// Suspend until the movies data has loaded.
const { data: movies } = moviesResource.read()
return (...)
}
No hook lint errors, no waterfalls, data guaranteed to be available after the call to read()
.
If you want to do dependent fetching on more than one resource at the same time, only use suspense for the last hook call in the dependency chain.
@johanobergman It looks great! One limitation comparing to the original proposal is missing the ability to do "Promise.race", for example:
useSWRSuspenseStart()
const { data: foo } = useSWR('/foo')
const { data: bar } = useSWR('/bar')
// either `foo.id` or `bar.id` is ok here
const id = foo ? foo.id : bar ? bar.id : null
const { data: baz } = useSWR(id ? '/baz?id=' + id : null)
useSWRSuspenseEnd()
When the dependency gets more complicated, there is no easy way to be as parallelized as this API. The "preload + read" approach somehow still "defines" the order before blocking, while the wrapper solution works like a solver.
I'm not sure if this is critical in real-world applications though, but still a limitation.
@shuding Maybe make the api both suspense and not suspense at the same time?
function App() {
// fooData and barData are only meant for dependent fetching, can be undefined.
const { resource: fooResource, data: fooData } = useSWRSuspense('/foo')
const { resource: barResource, data: barData } = useSWRSuspense('/bar')
const id = fooData ? fooData.id : barData ? barData.id : null
const { resource: bazResource } = useSWRSuspense(id ? '/baz?id=' + id : null)
// Every fetch is started, so we can block here.
const { data: foo } = fooResource.read()
const { data: bar } = barResource.read()
const { data: baz } = bazResource.read()
return (...)
}
Maybe there's no need for a useSwrSuspense
hook at all, you could just include a "suspense handle" in every call to useSwr
.
function App() {
// Regular useSWR() calls, returns an optional suspense resource/handle as well as the data.
const { resource: fooResource, data: fooData } = useSWR('/foo')
const { resource: barResource, data: barData } = useSWR('/bar')
const id = fooData ? fooData.id : barData ? barData.id : null
const { resource: bazResource } = useSWR(id ? '/baz?id=' + id : null)
// Use read() to block, or check loading states as normal.
const { data: foo } = fooResource.read()
const { data: bar } = barResource.read()
const { data: baz } = bazResource.read()
// To make it (possibly) more explicit, you could read through a "blocker" function instead.
const [foo, bar, baz] = suspenseBlocker([fooResource, barResource, bazResource])
return (...)
}
Yeah that's a great idea too! 👍
Althought probably pretty late to the party, this is the API that I ended up with when working on resolving the same issue on my vue port of SWR:
<script setup>
import { querySuspense } from 'vswr'
// Notice we don't use await here, and the result of those `querySuspense`
// are plain promises.
const first = querySuspense('/some/query') // Imagine this takes 2 seconds
const second = querySuspense('/some/other/query') // Imagine this takes 2 seconds
// This await will block until both resolve, but both already
// started fetching so the blockage is 2 seconds instead of
// 4 seconds if we awaited each one individually.
const [{ data: firstData }, { data: secondData }] = await Promise.all([first, second])
</script>
<template>
<!-- Do something with firstData and secondData -->
</template>
More: https://github.com/ConsoleTVs/vswr#parallel-suspense
is this PR... suspended?
I am also curious about progress on this PR
Any update on this?
@huozhi Do you know any updates on this? You seem to be active here :)