Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

RFC: QueryStrictMode for Prefetching #8064

Unanswered
TkDodo asked this question in Ideas
Sep 18, 2024 · 8 comments · 12 replies
Discussion options

Prefetching is hard, but it’s also one of the best ways to get data to your users as fast as possible. There are also multiple ways to prefetch, the most common ones are:

  • on the server: with server-side-rendering or fetching in server components, we can seed the cache before the component renders on the client.
  • in route loaders: in an SPA, the route loader is probably as high-up in the tree as possible.
  • in a root component with usePrefetchQuery
  • in event handlers with queryClient.prefetchQuery

Prefetching or "hoisting your data requirements" is especially recommended when using suspense, because it can easily trigger request waterfalls: Calling useSuspenseQuery twice in the same component gives you a waterfall. Calling it in siblings within the sam suspense boundary almost gave us a waterfall in React19.

The problem

The main problem we are seeing with prefetching is code-dislocation. Just calling useSuspenseQuery in your component and having it work is very convenient, but without a compiler like relay, it won’t be possible to extract those data requirements and trigger prefetching somewhere else automatically.

So what we are left with is duplicating what we do: Have a component call useSuspenseQuery with certain options, and use the same options in your route loader or event handler to do prefetching. That’s an okay trade-off, however, it comes with two additional problems:

Under-prefetching

Let’s say we write a component that fetches from /issues/$id , so we add prefetching for that to our route loader. Everything is fine and we ship it. Next, someone else adds fetching comments for that issue to the component, so there will be a new query that fetches from /issues/$id/comments. It’s quite common to forget to add that fetch to the route loader, which leaves us with a new request waterfall: We are only stating to fetch comments after we are done fetching the issue details, even though they could load in parallel.

Over-prefetching

On the flip-side, let’s assume we do prefetch issue details and comments correctly in the route loader, but then, we decide to move comments to an entirely different page. When the component moves, so does the query, but the prefetching in the route loader might stay. That will fire an unnecessary request for /issues/$id/comments , and it might even block the page from loading until that request is done.

Proposed solution

To address both problems, the idea is to introduce a <QueryStrictMode> component in DEV mode only (similar to the React StrictMode component) that will do the following:

  • Whenever useSuspenseQuery would suspend the component with a new promise, it would log an error. This will alert developers that they are using a suspense query, but haven’t prefetched it. When the component renders, it should merely pick up a promise that is already in the cache. This would address the under-prefetching issue.
  • Whenever a Query isn’t used within a given time frame (probably 5ish seconds?) after it has been prefetched, an error will be logged. This will alert developers that they have prefetched something that could be unnecessary. This would address the over-prefetching issue.

Potentially, we could make this component’s logger customisable (defaulting to console.error) so that you can opt out of logging for certain queries:

<QueryStrictMode
 logger={
 (props: { type: 'prefech-missing' | 'unnecessary-prefetch', message: string, query: Query }) =>
 console.error(message)
 }
>
{ children }
</QueryStrictMode>

Implementation details

Under-prefetching protection is relatively simple to implement: We can check before we throw fetchOptimistic if there is a promise in the cache already. If not, fetchOptimistic would trigger the fetch, which isn’t really what we want.

Over-prefetching protection is a bit more involved. I think we could subscribe to the QueryCache directly, and whenever a fetch starts while there is no active observer for the given key, we consider it a prefetch (note to self: we have to ignore while we’re in isRestoring mode because restores from an external storage shouldn’t be considered prefetches). Then, we start a timer which we can cancel as soon as an observer subscribes or the query gets garbage collected.

You must be logged in to vote

Replies: 8 comments 12 replies

Comment options

TkDodo
Sep 18, 2024
Maintainer Author

@Ephem one open question that came to my mind today is how this would integrate with the planned experimental_prefetchInRender flag combined with the React.use() operator. I don't think we can warn here as well when the prefetchInRender fires a fetch, as it would make the whole feature obsolete: If you have a running promise already, you will never need to fetch-in-render...

You must be logged in to vote
0 replies
Comment options

Excited that RQ is thinking about this! We've been working on our own enforcement of this on top but it'd be nice to have it built in.

One concern with the Context provider approach is that it makes it harder to do incremental migrations of existing code. In our experience you rarely have nice and neat "clear" branches of the app you can cordon off to enforce prefetching in. In practice we pull up queries one at a time as we have time to track down requests in component trees and refactor fetching logic out of hooks so they can be prefetched by the router. This can take some time before we can say "don't allow fetching in render here".

One option is to have something like usePrefetchedQuer(y|ies) (or query option) that marks individual call sites as always expecting data for the under prefetching cases.

You must be logged in to vote
4 replies
Comment options

TkDodo Sep 18, 2024
Maintainer Author

that marks individual call sites as always expecting data

that really is useSuspenseQuery to me. If you useSuspenseQuery without prefetching, you will have waterfalls as soon as you have more than one query.

Comment options

I don't disagree with that. My point is more that we already have large app with suspense queries and waterfalls we want to remove, If we wrap them all in QueryStrictMode, the logs will just be noise, and errors unhelpful, until we can add prefetching for each of them.

I do also think there are cases where you intentionally want a waterfall still want to use suspense to manage loading behavior, but i'm less confident about that use case.

Comment options

TkDodo Sep 18, 2024
Maintainer Author

got it. that's where I was thinking that you can customize the logger to ignore certain "known waterfalls".

I don't think we should make a hook that says "I need a prefetch to have happened here":

function Component() {
 useThisShouldBePrefeteched(todoOptions)
 const { data } = useSuspenseQuery(todoOptions)
}

because that would be an opt-in behaviour on a per-use-case basis. (Almost) no one would do that. Imagine the exhaustive-deps lint rule to only scream at you when you say "I want this deps to be exhaustive". Not good.

If you know that you need prefetching in a certain place, you are already far ahead of the curve. At this point, you can call usePrefetchQuery somewhere higher up the tree, too. Most people don't know that they get waterfalls when they write useSuspenseQuery, and that's where the QueryStrictMode is valuable: To avoid them being introduced accidentally.

Comment options

What about going the opposite way and adding an option to useSuspenseQuery that specifically opts it out of prefetch enforcement?

useSuspenseQuery({..., ensurePrefetch: false})

or allowWaterfalls etc.

Comment options

Will there be support for regular useQuery? It might not create as bad waterfalls as useSuspenseQuery but it still makes a difference when data is prefetched in route loaders, especially with lazy loading. There could be an option for that as some queries might be non-prefetchable for various reasons.

You must be logged in to vote
2 replies
Comment options

TkDodo Sep 19, 2024
Maintainer Author

Would you have an idea how we could support that? How should we or the user decide which queries should warn?

Comment options

Maybe something like this, but with less ugly name:

const { data } = useQuery({ ..., warnIfNotPrefetched: true })

It could even be false by default, user would still be able to change this when creating QueryClient instance, by using defaultOptions. It could be adjusted per query level when needed using code above.

Comment options

Just wanted to clarify that on over-fetching, this:

Whenever a Query isn’t used within a given time frame (probably 5ish seconds?) after it has been prefetched...

wherein you say "after it has been prefetched" means this (configurable?) timer is only started once the cache has been populated with the prefetched data, not started from when the query is started, correct?

You must be logged in to vote
1 reply
Comment options

TkDodo Nov 27, 2024
Maintainer Author

yes, correct.

Comment options

Just calling useSuspenseQuery in your component and having it work is very convenient, but without a compiler like relay, it won’t be possible to extract those data requirements and trigger prefetching somewhere else automatically.

I don't really have anything to add to the <QueryStrictMode> discussion here. It seems fine, but what is stopping React Query from adding a compiler to automatically hoist and prefetch data? Admittedly, graphql does lend itself very well to the whole idea of defining fragments in your component and having a compiler statically extract that into queries higher up the component tree. It also helps that Relay is only scoped to graphql whereas React Query is really just an async data loader.

If at all possible, would you be able to provide some details why adding a compiler to React Query would be a bad idea or wouldn't work? I really don't know what it would look like but it would be valuable knowing more details beyond the quoted statement above.

Yes, without a compiler this all gets tricky and validation moves to runtime. But, why can't React Query introduce a compiler?

You must be logged in to vote
3 replies
Comment options

TkDodo Jan 10, 2025
Maintainer Author

but what is stopping React Query from adding a compiler to automatically hoist and prefetch data?

not sure what to say other than TanStack is an ecosystem of open source libraries that are maintained by handful of people in their free-time. We don’t have the same resources as facebook has to write a compiler - unless you are maybe volunteering to write one 😉 ?

Comment options

Also note that relay's compiler doesn't hoist anything it just turns static fragments into runtime accessible type data, and concatenates GQL fragments into a single query string. There isn't any hoisting in Relay, the fetching is always where you say the query is. I say this simply to note that making a compiler actually move data fetch calls somewhere else is a much harder problem

Comment options

but what is stopping React Query from adding a compiler to automatically hoist and prefetch data?

not sure what to say other than TanStack is an ecosystem of open source libraries that are maintained by handful of people in their free-time. We don’t have the same resources as facebook has to write a compiler - unless you are maybe volunteering to write one 😉 ?

Yea I'm not opposed the that. Just wanted to make sure there wasn't an intentional decision to not implement a compiler.

@jquense is right though- relay doesn't really automatically hoist. Still requires dev to pull in children queries. I'll poke around and see what I can come up with. Likely would be limited to rest APIs with an accurate swagger file

Comment options

This might be only tangentially related but I wanted to share a use case we've run into that might be relevant.

We have certain pieces of data that are only fetchable on the server, ie. there is no corresponding client-accessible API to fetch them from the client. We fit this into our existing React Query data setup by defining a useSuspenseQuery call that looks for a given query key, but does not provide any queryFn. We then make sure to "prefetch" that query on the server so that the data is always available on the client. This works as long as the prefetching is working properly, but we've run into cases where we get a "Missing queryFn" error because the prefetched data wasn't there, or was garbage collected or something. In order to prevent that we have to make sure that React Query never removes or tries to refetch the data by setting staletime and gc time to infinity.

I wonder if it would make sense in a strict prefetching tree to change the behaviour of a useSuspenseQuery that lacks a queryFn, so that it automatically preserves its data forever and never attempts to call the missing queryFn, just logging an error if the data is not prefetched already.

You must be logged in to vote
1 reply
Comment options

TkDodo Jan 10, 2025
Maintainer Author

we don’t really support using a query without a queryFn, basically for the reasons you outlined: it’s an implicit coupling to something that must have happened before, but there’s no way to guarantee that it happened.

for your case, you’re probably better off putting that data in a react context or state manager like zustand. React Query is a data synchronization tool, and if you don’t want any data synchronization because you have no queryFn, you likely don’t need it.

Comment options

Unsure if this falls under the same problem you are trying to solve, but if we do list.prefetchInfinite() and combine it with list.useSuspenseQuery() it will cause loading / suspense to flicker twice. Changing to list.useSuspenseInfiniteQuery() resolves the issue.

Basically enforce a warning to match proper prefetch with proper useSuspense.

Syntax written above is from tRPC.

You must be logged in to vote
1 reply
Comment options

TkDodo Jan 10, 2025
Maintainer Author

This is indeed a different issue. Infinite Queries and Finite Queries (normal queries?) have a different structure, and mixing them is not possible. I’ve written about this before: https://tkdodo.eu/blog/effective-react-query-keys#caching-data

We could try to detect this in dev mode and log better warnings, but since this never works, it’s not about StrictMode or not.

Comment options

I really like this RFC, and I can't think of a better way to implement it, as you said there is only one trade-off we're left with...

One issue might be, when you want to mix some hoisted requirements (ensure prefetching for some things), while letting some Suspense boundary intentionally in place, and that would then be throwing the warning even though it is not useful. The proposed #8064 (reply in thread) and the counter-argument is valid, so I get why we'd want to use a top-level <QueryStrictMode>, perhaps the way to "silence" the known waterfalls is to use a hook that preceeds the upcoming useSuspenseQuery() (that would trigger a waterfall):

useMuteQueryStrictMode(todoOptions)

This way it would be co-located, can be a noop for non-DEV and can still issue a specific type for the logger, like known-missing-prefetch for instance.

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /