v5 Roadmap 🗺 #4252
Replies: 26 comments 94 replies
-
|
+1 for removing |
Beta Was this translation helpful? Give feedback.
-
|
Cool stuff overall!
I would understand if we renamed an option to something shorter if part of it was redudant, for instance: const query = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})would turn into // (not actually suggesting this should be a change on v5)
const query = useQuery({
key: ['todos'],
fn: fetchTodos,
})but renaming an option to something that won't still be near 100% perceptible seems wrong to me :P What do you think? |
Beta Was this translation helpful? Give feedback.
-
|
Hi, this is so well though plan. CODEMODMaybe it's too early to talk about tooling, but i guess we can at least list codemod options: This could take care of:
LoggerThis one i'm not sure about the reasonning. |
Beta Was this translation helpful? Give feedback.
-
|
This might be a long shot, but would you consider supporting
|
Beta Was this translation helpful? Give feedback.
-
RE: rename cacheTimeI think the need for better name is great and your highlighted exhibits are definitely good reasons to do it. However, I don't think
On Twitter, I pitched @TkDodo the idea of |
Beta Was this translation helpful? Give feedback.
-
RE: remove overloadsThis is a fantastic consideration! Would it maybe be better to go with: useQuery(queryKey, options)as the only signature. As you mentioned, |
Beta Was this translation helpful? Give feedback.
-
rename useErrorBoundaryI agree ErrorBoundary is very much a React concept, but the |
Beta Was this translation helpful? Give feedback.
-
Remove Overloads will make
|
Beta Was this translation helpful? Give feedback.
-
Update October 2nd:I added two more points to the list:
please take a look :) |
Beta Was this translation helpful? Give feedback.
-
Context SharingSo i'm a user of context sharing for microfrontends. Relying only on bundlers to share a common library is only one part of the equation. It's all related to the order of context creation. With current context sharing, i don't have to pre instanciate context and pass it to all micro frontends. if i have two microfrontends that use react, they will share context (witchever one is instanciated first). While if i have to instanciate a common context, i'll have to pass it manually to all microfrontends (even though at instanciation the host don't know if the microfrontend is using react and then instanciate context instance in the host even though i don't need it in the host or even don't need it in all microfrontends. This would really complexify microfrontends. Because i will not want to instanciate context in the host. So i will need to encapsulate context in a shared microfrontend or shared common library so that only microfrontends that need it will use it. I would really prefer if this option stay like it is and is not deprecated |
Beta Was this translation helpful? Give feedback.
-
|
Thanks for all your hard work, and great ideas as always. useQuery({
queryKey,
queryFn,
cacheFn: <T>(data: TQueryFnData): [QueryKey, T][]
}) |
Beta Was this translation helpful? Give feedback.
-
|
I have one, high-level dream - to make optimistic update of pagination easier. |
Beta Was this translation helpful? Give feedback.
-
|
About the It would be amazing a way to reuse |
Beta Was this translation helpful? Give feedback.
-
|
@TkDodo Use case for custom logger: Run tests in development mode ( |
Beta Was this translation helpful? Give feedback.
-
|
Thanks for the transparency on the roadmap! I have a question about Pending with regards to Promises in JS implies that there is an instance of a promise, and it is yet to fulfill or reject. If i understand the current proposal correctly, Furthermore would this get confusing as we start to think about the future of the I mention as the goal seemed to be to clear up confusion of the way things are represented currently, but i'm hoping we aren't introducing new confusion 😃 |
Beta Was this translation helpful? Give feedback.
-
|
Since it's a big subject on it's own and not all of it necessarily related to v5 (some can be done in minors), I posted a braindump on my current thinking around React SSR/Server Components over in this discussion. Some of the actionable things can probably be cross-posted here in the roadmap already, or do it as a umbrella issue thingy, and some we can copy in here when we've figured them out? |
Beta Was this translation helpful? Give feedback.
-
|
just my vote... |
Beta Was this translation helpful? Give feedback.
-
|
@TkDodo, I noticed that while the plan is to have one signature for all methods and that's a good change IMO, the current ESLint rule only checks for queries (ref). Could the rule be extended to match the other methods too, e.g. PS: Sorry if many people get pinged with this comment |
Beta Was this translation helpful? Give feedback.
-
|
Could some functionality that exists in community libraries such as |
Beta Was this translation helpful? Give feedback.
-
|
@TkDodo I followed the instruction in the documentation |
Beta Was this translation helpful? Give feedback.
-
|
Based on the open tickets seen on milestone/2 linked at the top.. I wonder if there are any open issues NOT added to this milestone, which would give most CSR folks pause? (my team/project isn't using react query yet, so I'd rather not write new code which will get deprecated in a few months) |
Beta Was this translation helpful? Give feedback.
-
|
@TkDodo with the new breaking change, removing |
Beta Was this translation helpful? Give feedback.
-
|
Hello everyone. Is it possible to somehow share data between vue-router and vue-query? My use case is: I need to fetch user profile and some other data from server to understand, can user go to some route or not. And I want to use all benefits of vue-query for that. Now we can use vue-query only inside vue components which is a problem, at least in my case. So is it possible workaround? In features releases or maybe I missed something? Thanks. |
Beta Was this translation helpful? Give feedback.
-
|
I might've missed it, is there any prediction on when v5 will be officially released? |
Beta Was this translation helpful? Give feedback.
-
|
The proposal for changing the loading/fetching flags sounds great. Having a simple |
Beta Was this translation helpful? Give feedback.
-
|
Thanks for this new awesome version. Really like the new query status approach. From migrating to v5 from v4 there is a missing case, that maybe is intentional. On v4 NOW on v5 I would like to understand this intention :) PS There is a misleading definition related to the above statement on |
Beta Was this translation helpful? Give feedback.




Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
TanStack Query v5 Roadmap
Update: The roadmap is now a milestone:
This is a first draft of what a v5 could look like in terms of breaking changes. Nothing of this is set in stone, and no implementation on anything here has started. This discussion should serve as an umbrella to track things we might want to do, and to find out very early if some proposals are not a good idea.
Please feel free to add your thoughts and ideas as comments:
status: pending
see details
context
In v4, we removed the idle state because it was only used in conjunction with disabled queries and led to a bunch of impossible states with the introduction of the new fetchStatus. You couldn't be in
idlestate and befetchingorpausedat the same time. This was the only exception to the fact that all combinations ofstatusandfetchStatuswere valid, so we removed it.In summary, we now have:
status:
loadingsuccesserrorfetchStatus:
idlefetchingpausedThis led to the unfortunate situation that you cannot easily check when to display a loading spinner if your query is disabled, even if you use the
enabledoption conditionally, e.g. for lazy queries, because the query will start inloadingstate if it doesn't have data yet, even if it's not fetching because it's disabled.So you had to combine two checks (
isLoading&&isFetching), which is why we introduced a new flag in v4.8.0 called isInitialLoading that combines the two.However,
isLoadingwould be a much better name for this condition. It literally means: show a loading spinner! What we currently have asloadingstate is a bit misleading because it doesn't mean "data is loading", it just means "we have no data yet". It is used to discriminate thedatafield - if you check forisSuccess, it is guaranteed thatdatais defined.A better name for this mindset would be
pending.pendingas in "the promise is still pending" or "we don't have data yet". It doesn't say much about data being fetched or not. A disabled query can very well be in pending state - you wouldn't necessarily show a loading spinner just because data is pending.proposal
status: loadingtostatus: pendingand the derived booleanisLoadingtoisPendingisLoadingthat is implemented asisPending && isFetchingIf we do this,
isLoadingandisInitialLoadingwill have the same meaning. We could just flat out removeisInitialLoadingand tell people to go back toisLoadingnow, but I fear that this will not land well with the community because we just told them to go fromisLoadingtoisInitialLoadingwith v4.8.0.So I would deprecate
isInitialLoadingwith v5, but keep it intact (just an alias forisLoadingreally) and remove it in v6.remove overloads
see details
context
useQueryand friends have many overloads in TypeScript - different ways how the function can be invoked. As an example, let's look atuseQuery. You can call it in three different ways:The
queryKeyis the only required property. The queryFn is optional because you might have defined it globally. If you use the syntax where you only pass one object in, the queryKey is also required.Not only is this tough to maintain, type wise, it also requires a runtime check to see which type the first and the second parameter are to correctly create options. Internally, we only work with one options object.
Further, we started to add more overloads for features that are only doable with overloads. For example, if you pass
initialData, the return type ofdatawill not containundefined. BecauseinitialDatacan also be a function, we need another 3 overloads for each possibility to calluseQuery, resulting in 9 total overloads.The
useQuery.tsfile has 140 lines of code - only 3 of which are actual JavaScript.Other functions suffer from the same problem, and overloads are also not consistent. For example,
queryCache.findAllhas overloads, butmutationCache.findAlldoes not.proposal
This means that
useQuerywould need to be called:This does not only affect
useQuery, but all functions that accept overloads:useMutation(() => axios.post(...))will becomeuseMuation({ mutationFn: () => axios.post(...) })invalidateQueries(["todos"])will becomeinvalidateQueries({ queryKey: ["todos"] })and so on...
We have actually tried this already for v4, see:
but there was a type inference issue that stopped the attempt. The issue was fixed with TS 4.7:
mitigation strategy
We will very likely provide a codemod that automatically transforms your queries to that syntax
require minimum of TS 4.7
see details
v4 supports TS4.1 or greater, v5 will need TS4.7 or greater for the reasons mentioned above. It might work fine with TS4.1, but you might run into type inference issues.
remove custom logger
see details
context
In v3, we had a global logger that you could set via
setLoggerto your own logging mechanism. This was a side effect which we tried to get rid of, so in v4, you can now pass aloggerprop to theQueryClient.The logger mainly does one thing: It logs failed queries to the console. This is fine for development, but it was confusing to many that it also showed up in production.
That's why we've removed all logging in production in v4, and we've also started to use the logger more to show development specific warnings, for example:
queryKeythat isn't an Array.undefinedfrom your queryFn.Those logs are meant to help developers find potential issues in their code. They are not meant to be shown in production. I'd see them on the same level as the warning you get from React in development mode, for example:
There is also no way to show these warnings in production. This also helps with bundle size because all logging happens behind an env check, so it is stripped from the final bundle. This means we can be as verbose as we want with those messages.
All of this means that the custom logger is kind of unnecessary. Why would you want to pass a custom logger only to log differently in development mode? The
loggerprop itself cannot be tree-shaken, so it will always be included in the final bundle even though we actually never use it.proposal
loggerprop fromQueryClientWe should be able to just use the
consolefor those development logs as it exists in all environments.make TError default to Error instead of unknown
see details
context
In JavaScript, you can
throwanything, even literals like5or Promises (?? suspense). That is why the generic for the typeTErrordefaults tounknown. This is in line with how TypeScript itself handles errors in catch clauses since v4.4.This is not very practical. Unless you explicitly throw a non-error, error will be at least of type
Error. If you useaxios, it might be of typeAxiosError, but that is also not guaranteed. For example, if you have a runtime error inselect, that will be caught, and it puts your query intoerrorstate. The error will be of typeErrorthen.Practically, it means that you either fall back to runtime checks:
or, you pass a generic to
useQuery:The second approach is bad for two reasons:
TErroris the second generic, so you also have to annotate the first generic, which is the return type of the queryFn. This could actually be inferred.useQueryhas 4 generics, and if you only provide two of them, the other two will fall back to their default value instead of being inferred from their usage. That meansselectwill not work as expected:You'll get a weird error about no overload matching, see this TypeScript Playground.
proposal
TErrorgeneric default toErrorinstead ofunknownThis will have to happen here:
query/packages/query-core/src/retryer.ts
Lines 143 to 148 in ca76777
catch (error) { - promiseOrValue = Promise.reject(error) + const properError = error instanceof Error ? error : new Error(String(error)) + promiseOrValue = Promise.reject(properError) }rename useErrorBoundary
see details
context
A property that starts with the prefix
useis unfortunately named, asuseusually indicates a hook in React.ErrorBoundarymight also be a quite react specific term.Renaming the property to e.g.
throwErrorwould more accurately describe what is happening: the error will be thrown. In React, it will be picked up by the nearest error boundary.proposal
useErrorBoundarytothrowErrorrename cacheTime
see details
context
Almost everyone gets
cacheTimewrong (exhibit A). It sounds like "the amount of time that data is cached for", but that is not correct.cacheTimedoes nothing as long as a query is still in used. It only kicks in as soon as the query becomes unused. After the time has passed, data will be "garbage collected" to avoid the cache from growing (see also this explanation).RTK Query has the same feature - their prop is called keepUnusedDataFor. I think this is quite descriptive but also a bit long.
proposal
cacheTimetogcTimegcis referring to "garbage collect" time. It's a bit more technical, but also a quite well known abbreviation in computer science.Also, it is not something that most people will have to customize. The default of 5 minutes is usually fine. A rename will reduce the chance that this property is mixed up with
staleTime.Lastly, if someone doesn't immediately know what
gcTimestands for, they will (hopefully) consult the docs. This is a lot better than thinking they know whatcacheTimedoes.Here is an old discussion on that topic:
Alternatives:
After some discussions, a rename to
inactiveCacheTimewould also be possible. It' similar to the current name but clearly indicates: "The time that inactive queries will be cached for". It can take a way a lot of confusion about the cacheTime being valid for all queries, even those that are actively used (= active queries).Inactiveis also the naming we show in the devtools as well as when using QueryFilters, so it fits well.size improvements
see details
context
The "big" bundle size is a constant topic when TanStack Query is compared to other libraries. I think there are a few things that can be done to improve the situation:
don't transpile optional chaining
Optional chaining is used a lot in the codebase, and it's supported in 93.44% of all browsers. Transpiling it is quite costly:
switch to private class fields and methods
Private class fields are supported in 92.7% of all browsers. They have a bunch of advantages:
proposal
Out of these changes,
Safariwould be the "newest" supported browser, with a release date of September 2021. This would still likely mean at least 1.5 years of browser support for Safari when we release v5.If people want to support older browsers, they can always transpile the code themselves.
privateTypeScript keyword.remove
isDataEqualproperty on useQuerysee details
We have two props that go together on
useQuerythat are around structural sharing:isDataEqual?: (oldData: TData | undefined, newData: TData) => booleanundefinedstructuralSharing?: boolean | ((oldData: TData | undefined, newData: TData) => TData)trueThis is how it works internally when new data comes in, and we need to consolidate it with existing data:
isDataEqualis passed. If it is, and it returns true, we just return the previousData.structuralSharingis on (boolean). If it's not, we returned the new datastructuralSharingis on, we try to re-use as much as possible frompreviousDatato keep referential identity. This sharing is not for free, so you can turn it off. It also only works on json compatible values per default.With v4.2.0, we added a feature for custom
structuralSharingfunctions. This way, consumers can still achieve the performance benefits of retained references even when cache data contains non-serializable values.This feature has a nice side effect: you can now implement
isDataEqualwith it. All you need to do instead is do the same check in yourstructuralSharingfunction and returnoldDataif they are equal instead oftrue. The functions also have the exact same interface for the parameters passed in:proposal
isDataEqualin v4structuralSharing. For this to work, we also need to expose the internal functionality that structural sharing is doing (currently namedreplaceEqualDeep).isDataEqualfromuseQueryin v5remove contextSharing
see details
context
The
QueryClientProviderhas a propcontextSharing: boolean. The docs say:To be honest - I don't really know how this property is working. There were some discussion on this issue that suggest that it is not really useful.
For microfrontends, isolation is often preferred. With v4, we introduced the option to pass a custom context, which allows for exactly that.
If you want your app to use the same client when it's composed of multiple packages, all you'd need to do is create one QueryClient in your app and let the different bundles pick those up. As long as they all use the same version of TanStack Query, this should work fine.
If you have more info on how
contextSharingworks or where it is useful, please share!proposal
contextSharingin v4contextSharingfromQueryClientProviderin v5replace custom context with custom queryClient
see details
context
in v4, we introduced the possibility to pass a custom context to all react-query hooks. This allowed for proper isolation when using MicroFrontends. See the migration guide for examples and details.
However, context is a react only feature. All that context does is give us access to the query client. We could achieve the same isolation by allowing to pass in a custom query client instead of a custom context that then serves the query client.
vue-query does have this api already, so it makes sense to streamline it in react as well.
see also:
proposal
drop support for React17
see details
context
With nextJs 13 also requiring React18, it makes sense to follow suit. All new features are build on top of react18. This means we can also drop the
useSyncExternalStoreshim, which has caused troubles in the past with react-native and ESM, and also adds a bit of bundle size.proposal
remove
removereturned fromuseQuerysee details
context
removeremoves the query from the queryCache without informing observers about it. It is best used to remove data imperatively that is no longer needed, e.g. when logging a user out.It doesn't make much sense to do this while a query is still active, because it will just trigger a hard loading state with the next re-render. So when would you ever use the
removefunction returned from useQuery, because at that point, you still have an active observer ?anecdotal twitter thread
proposal
removein v5remove unstable_batchedUpdates
see details
context
we currently use
unstable_batchedUpdatesas ourbatchNotifyFnin React and React Native:query/packages/react-query/src/setBatchUpdatesFn.ts
Line 4 in 357ec04
Apparently, this function is a "noop" in React18, as discussed here:
facebook/react#24831 (comment)
Unanswered questions:
proposal
query/packages/react-query/package.json
Lines 24 to 26 in 9b21609
remove
queryHashfrom useQuery optionssee details
context
The
queryHashis the result of the query key hashing, produced by thequeryKeyHashFn. This function defaults to a stableJSON.stringify, but can also be passed in by users to do custom hashing, e.g. when non-json serializable things are used in thequeryKey.According to our TypeScript types, we can also pass in a
queryHashdirectly intouseQuery. This is not only undocumented, it's also redundant because you can achieve the same thing with aqueryKeyHashFn:proposal
queryHashfrom useQuery optionsexpose custom cache
see details
context
Inspired by swr cache provider, we can allow to pass a "custom cache" into our QueryCache:
The cache would have to adhere to a map-like interface, and we should probably re-write our internal object to a standard
Map.This would allow features like:
proposal
Mapfix pageParams in infinite queries
see details
context
there are two issues related to
pageParamsthat we likely cannot fix in a backwards compatible way:proposal
pageParaminInfiniteData(this would fix Infinite Queries: wrong auto refetching with manual pageParams #4464)manualflag to find out ifpageshould be used for refetching or not.nullfromgetNextPageParam(this would fix null is passed as pageParam with PersistQueryClientProvider #4309)nullandundefinedwould both be treated as "no next page available"nullso that we can json serialize itundefinedto thequeryFnso that default value assignment would still work:remove placeholderData as a functionwe'll do this instead:
see details
context
The real motivation is that we can remove a lot of internal code if we drop this feature, and there's no real gain to use a function over a value or a memoized value. To achieve memoization, you had to pass in a stable function (probably with
useCallback). Since the function receives no input, you can just as well do this withuseMemo. Unlike theinitialDatafunction, which only runs once per cache key, the placeholderData function runs on every render if placeholder data needs to be shown. So the advantage of it being potentially a function is minimal.proposal
Beta Was this translation helpful? Give feedback.
All reactions