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.
-
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
idle
state and befetching
orpaused
at the same time. This was the only exception to the fact that all combinations ofstatus
andfetchStatus
were valid, so we removed it.In summary, we now have:
status:
loading
success
error
fetchStatus:
idle
fetching
paused
This 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
enabled
option conditionally, e.g. for lazy queries, because the query will start inloading
state 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,
isLoading
would be a much better name for this condition. It literally means: show a loading spinner! What we currently have asloading
state 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 thedata
field - if you check forisSuccess
, it is guaranteed thatdata
is defined.A better name for this mindset would be
pending
.pending
as 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: loading
tostatus: pending
and the derived booleanisLoading
toisPending
isLoading
that is implemented asisPending && isFetching
If we do this,
isLoading
andisInitialLoading
will have the same meaning. We could just flat out removeisInitialLoading
and tell people to go back toisLoading
now, but I fear that this will not land well with the community because we just told them to go fromisLoading
toisInitialLoading
with v4.8.0.So I would deprecate
isInitialLoading
with v5, but keep it intact (just an alias forisLoading
really) and remove it in v6.remove overloads
see details
context
useQuery
and 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
queryKey
is 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 ofdata
will not containundefined
. BecauseinitialData
can also be a function, we need another 3 overloads for each possibility to calluseQuery
, resulting in 9 total overloads.The
useQuery.ts
file 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.findAll
has overloads, butmutationCache.findAll
does not.proposal
This means that
useQuery
would 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
setLogger
to your own logging mechanism. This was a side effect which we tried to get rid of, so in v4, you can now pass alogger
prop 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:
queryKey
that isn't an Array.undefined
from 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
logger
prop itself cannot be tree-shaken, so it will always be included in the final bundle even though we actually never use it.proposal
logger
prop fromQueryClient
We should be able to just use the
console
for those development logs as it exists in all environments.make TError default to Error instead of unknown
see details
context
In JavaScript, you can
throw
anything, even literals like5
or Promises (?? suspense). That is why the generic for the typeTError
defaults 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 intoerror
state. The error will be of typeError
then.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:
TError
is 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.useQuery
has 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 meansselect
will not work as expected:You'll get a weird error about no overload matching, see this TypeScript Playground.
proposal
TError
generic default toError
instead ofunknown
This will have to happen here:
query/packages/query-core/src/retryer.ts
Lines 143 to 148 in ca76777
rename useErrorBoundary
see details
context
A property that starts with the prefix
use
is unfortunately named, asuse
usually indicates a hook in React.ErrorBoundary
might also be a quite react specific term.Renaming the property to e.g.
throwError
would more accurately describe what is happening: the error will be thrown. In React, it will be picked up by the nearest error boundary.proposal
useErrorBoundary
tothrowError
rename cacheTime
see details
context
Almost everyone gets
cacheTime
wrong (exhibit A). It sounds like "the amount of time that data is cached for", but that is not correct.cacheTime
does 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
cacheTime
togcTime
gc
is 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
gcTime
stands for, they will (hopefully) consult the docs. This is a lot better than thinking they know whatcacheTime
does.Here is an old discussion on that topic:
Alternatives:
After some discussions, a rename to
inactiveCacheTime
would 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).Inactive
is 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,
Safari
would 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.
private
TypeScript keyword.remove
isDataEqual
property on useQuerysee details
We have two props that go together on
useQuery
that are around structural sharing:isDataEqual?: (oldData: TData | undefined, newData: TData) => boolean
undefined
structuralSharing?: boolean | ((oldData: TData | undefined, newData: TData) => TData)
true
This is how it works internally when new data comes in, and we need to consolidate it with existing data:
isDataEqual
is passed. If it is, and it returns true, we just return the previousData.structuralSharing
is on (boolean
). If it's not, we returned the new datastructuralSharing
is on, we try to re-use as much as possible frompreviousData
to 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
structuralSharing
functions. 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
isDataEqual
with it. All you need to do instead is do the same check in yourstructuralSharing
function and returnoldData
if they are equal instead oftrue
. The functions also have the exact same interface for the parameters passed in:proposal
isDataEqual
in v4structuralSharing
. For this to work, we also need to expose the internal functionality that structural sharing is doing (currently namedreplaceEqualDeep
).isDataEqual
fromuseQuery
in v5remove contextSharing
see details
context
The
QueryClientProvider
has 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
contextSharing
works or where it is useful, please share!proposal
contextSharing
in v4contextSharing
fromQueryClientProvider
in 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
useSyncExternalStore
shim, which has caused troubles in the past with react-native and ESM, and also adds a bit of bundle size.proposal
remove
remove
returned fromuseQuery
see details
context
remove
removes 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
remove
function returned from useQuery, because at that point, you still have an active observer ?anecdotal twitter thread
proposal
remove
in v5remove unstable_batchedUpdates
see details
context
we currently use
unstable_batchedUpdates
as ourbatchNotifyFn
in 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
queryHash
from useQuery optionssee details
context
The
queryHash
is 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
queryHash
directly intouseQuery
. This is not only undocumented, it's also redundant because you can achieve the same thing with aqueryKeyHashFn
:proposal
queryHash
from 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
Map
fix pageParams in infinite queries
see details
context
there are two issues related to
pageParams
that we likely cannot fix in a backwards compatible way:proposal
pageParam
inInfiniteData
(this would fix Infinite Queries: wrong auto refetching with manual pageParams #4464)manual
flag to find out ifpage
should be used for refetching or not.null
fromgetNextPageParam
(this would fix null is passed as pageParam with PersistQueryClientProvider #4309)null
andundefined
would both be treated as "no next page available"null
so that we can json serialize itundefined
to thequeryFn
so 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 theinitialData
function, 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