-
-
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
RTK Query API pain points and rough spots feedback thread #3692
Comments
First of all, I love RTK and RTKq! I use this library on many projects! Currently, the real black spot for me is the infinite scroll management, as you mentioned. I've needed this feature several times and haven't found a solution that suits me perfectly. So I've either switched to classic pagination, or I've used a not-so-great technique that consists of using pagination... but increasing the number of items to fetch. 20 items then 40 then 60 etc . Another very minor point is the data property on queries:
Every time I have to rename data:
I think I would have preferred the same design as the mutations, so that I could directly name my datas with the name I want.
Right now I can't think of anything else, but I'll be back if I ever do! |
Thank you for the forum. Using a Single API for Various Libraries (w/ or w/o React)My RTK Query APIs are commonly used in projects that don't use React; especially in testing environments. However, when a project does use an RTK Query API in an environment with React, I'd like to have a way to apply the hooks to it without having to re-create the API. It would be excellent to have a pattern that implements React specific enhancements by passing it through a function... import { reactify } from '@reduxjs/toolkit/query/react';
import { myApi } from '@mylib/apis/myApi';
export const myReactApi = reactify(myApi);
// myReactApi now has react hooks! Or just have hooks that take an API/endpoint as an argument... import React from 'react';
import { useQuery } from '@reduxjs/toolkit/query/react';
import { myApi } from '@mylib/apis/myApi';
export const myFC: React.FC = () => {
//
const { data } = useQuery(
myApi.endpoints.getSomething,
{ /* queryArgs */},
{ /* queryOptions */}
);
return <pre>{JSON.stringify(data, null, 2)}</pre>
} |
Another suggestion for RTKQ. Apply a Custom Action per Endpoint(I don't think this capability exists without some major customizations. As far as I can tell it's not easily achievable. I understand there are a lot of powerful features in RTKQ, and some I might miss.) Normally, I'm building Queries that acts on a state. The state's reducers/slices should know nothing of the Query APIs. For example, I'd like to ask someone to build a RTK Query library for an API that populates our shared state (from a shared library) when invoked. I don't want to then go through all the Slices in our shared state library to add all those specific API matchers. I want RTK Query API to dispatch the actions we already have. That way, I can just plugin the RTK Query API in an application, invoke a query, and it populates the state how I want. Rough Code Exampleimport { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query";
import { myUpdateStuffAction } from '@shared/state/actions';
export const myApi = createApi({
reducerPath: "myApi",
baseQuery: fetchBaseQuery(),
endpoints: (builder) => ({
getStuff: builder.query({
query: () => ({
url: "/getStuff",
method: "GET",
action: myUpdateStuffAction,
transformResponse: (response, meta, arg) => {
const transformed = response.reduce(() => {
/** Do some data transformation **/
}, []);
// The return must match the payload of the action property
return transformed;
}
})
})
})
});
/**
* The `myApi.endpoints.getStuff.initiate()` adds a property to the action telling me
* which API & endpoint invoked it.
* @example
* {
* type: '@shared/myUpdateStuffAction',
* payload: { ... },
* apiFrom: '@myApi/getStuff'
* apiMeta: { ... }
* }
*/ Other ThoughtsHonestly, in my mind, RTK Query is a tool to cleverly handle how to dispatch actions on a redux store when fetching data. I don't think it needs to expand my store with its own reducers and redux middleware. That information could be scoped inside itself. That's my opinion though. |
When generating from large OpenApi specs, it works pretty well. But there are issues if you want to enhance your endpoints, especially when using typescript. If you try to normalize your endpoints then it will lose the correct type and throw typescript errors throughout your application. Also after generation the api file, it does not seem to be easy to add additional methods to consolidate multiple calls by modifying the generated file. Maybe I am wrong about some of this. Feel free to correct me. |
@nhayfield can you give a couple further details?
|
sorry by normalize just meant reindexing by id for faster lookups on the list type queries. consolidating calls i meant for calls that depend on one another it is better to chain them together. i would like to access response headers from the generated hooks. all of these are possible when building the routes from scratch. but they become fairly difficult when generating from an openapi spec and especially when using typescript |
@nhayfield can you show a concrete example of what a handwritten version of this looks like? I get the general sense of what you're saying, but I need more details to get a better sense of what the pain points are and what possible solutions we might come up with. I'm assuming that normalizing is something you would typically do with Where and how would you want to "chain calls together"? Where and how would you want to access response headers, in what code? |
Just wanted to chime in and express support for the proposal of unifying the API by @Dovakeidy:
I know this is not a change one makes lightly and understand the considerations one has to make before changing an API. However, this proposal makes immediate sense to me and the different APIs for Queries/Mutations has been a source of confusion in my team. |
#3506 const { data, error, isLoading } = useGetPokemonByNameQuery('bulbasaur'); |
@nhayfield : I still don't think I understand where in that query hook output you would expect to find and access the response headers. Something like It's important to remember that RTKQ, at its core, doesn't even know about HTTP at all. It just tracks some kind of async request's status, and the async function is supposed to return an object like I think you might want to try writing a custom version of |
doesnt matter where, as long as it could be accessed. could be metadata or headers. not sure the basequery is an option because these are the response headers instead of the request headers. |
From a usability perspective, there are two big draw-backs for me right now. First is the lack of official support for complex objects as inputs and outputs of an api endpiont. I have been able to work around it by turning off the serialization warnings and by taking advantage of the transformServerResponse callback, but official support for both serializing the endpoint arguments and for deserializing the response would really polish up the library. The second major thing is the bug around mutations and caching. If you mutate data and then attempt to refetch it immediately afterwards - if there was a pending request to fetch the data before the mutation, then the subsequent re-fetch erroneously returns the old cached data :/ This has prevented me from being able to take advantage of the caching features of this library |
@rwilliams3088 can you clarify what you mean by "lack of official support for complex objects? What's an example of that? |
For example: a Date object. I use a number of these throughout my API. By default, if you attempt to pass Date objects into or out and Api Endpoint, you are going to get errors from the serialization check - since, of course, you aren't supposed to pass object references via redux. It would be very inconvenient to make the user of an endpoint serialize all the data themselves before being able to use the endpoint. And it could be error-prone as well. For a complex object like a Date, the format that gets sent to the server may change for different endpoints. Most will be ISO8601 of course, but some of them may only want the date component, some may require timezone adjustments, etc. Similarly, when I get a Date back from the server, I want it deserialized back into a Date then and there - and I may want to perform a timezone adjustment as well (UTC => local time). So some basic configuration options for serialization and deserialization on the way into and out of redux would make things a lot smoother and not require work-arounds. You could name the serialization parameter |
Another, smaller request for efficiency: drop uneccessary state like |
@rwilliams3088 Generally, if you're redoing a lazy query, you want to have a re-render to get the new query data from the hook. I don't really see the problem ? |
Personally, I would love to have the ability to have an onSuccess/onError callback options for hooks for Mutations! Tanstack/React Query offers this and it's quite nice. |
@seanmcquaid What's the benefit of having callbacks as opposed to using (note that React Query is removing its callbacks for queries in the next major, but apparently not for mutations? https://tkdodo.eu/blog/breaking-react-querys-api-on-purpose ) |
@markerikson - Thank you for the insanely quick reply, you are the best! Good callout on mentioning that they're removing this from Queries and not mutations, that's why I only mentioned this for mutation hooks. I think it personally reads a bit better when you remove that async handling with mutations and can essentially just move that logic into an object provided to the hook itself. Instead of potentially needing try/catch in a different function for it. Just a preference! |
Once I get the data, yes I'll probably want to re-render - but I don't need to re-render at the time that a request is submitted, when the args change, which will occur prior to receiving the data. Nor do I want a re-render as the request goes through intermediate state changes. Also, in the case of multiple requests getting fired off - some of them maybe cancelled (for example: when filters/sorts change on the front-end such that previous requests are now irrelevant), so I don't need to re-render at all for those requests. It's not the end of the world if there are extra re-renders, but they are also completely unnecessary. One can add their own lastArgs state to their component easily enough if they are really interested in tracking it. |
I'm a very happy user of RTK query for a very data intensive desktop app. Some feedback off the top of my head:
|
@mjwvb : thanks! A number of folks have mentioned the idea of "canceling queries". Can you describe what you would expect to happen in that case? Also, what's the use case for invalidating individual entries? |
I think cancelling should abort the running promise for a given endpoint in two possible ways: Locally using an abort function as returned by e.g. useQuerySubscription, and globally by using tags in the same way as invalidateTags. The endpoint entry should then return an error state with a "cancelled" error code, in which I will be responsible to refetch. When it is cancelled after a refetch from invalidation: just cancel that request and keep the cached data. Our (simplified) use case for invalidating individual entries is a little bit more niche though, and maybe another pain point in itself. We have data grids in which the user is able to add more data columns after the rows have been loaded. We want the new columns to be fetched incrementally instead of refetching all columns again. Initially we thought serializeQueryArgs with forceRefetch could help us out here, but in the end it wasn't possible. We came up with a complicated solution in which the visible columns are tracked in a global class outside the endpoint, linked using some sort of ID. Then in onCacheEntryAdded we listen for a visibleColumnsChange event and then try to fetch the extra columns. When the fetch request for the new columns has failed, we simply invalidate that cache entry so it will refetch all rows for all the visible columns. That's when a invalidateCacheEntry would be nice to have :). Sounds way too complicated, however we already had the class instance in place for other purposes so it was relatively easy to implement. Anyway besides invalidateCacheEntry, I think the incremental fetching of data is a rough spot on its own. |
Now that I think about it, I'm unsure why serializeQueryArgs/forceRefetch/merge didn't provide the solution... Theoretically it should be possible if I'm not mistaken? Our complicated implementation was before the availability of serializeQueryArgs etc., so it was already working and not high on the prio list to be refactored. Gonna look into it again tomorrow. |
Tossing out a few things that I know have come up a number of times:
|
I struggle with cache and optimistic updates, specially when I have: fetchAllOfX -> Saves in one cache wish I could normalize the cache or customise it in some way that allows to share ir between URLs |
Would love a way to reset the data in a useQuery hook. This would be helpful for for autocomplete searchboxes in particular. Screen.Recording.2023-09-18.at.10.58.47.PM.movWhen the user clicks an item in the autocomplete dropdown, I reset the search query to a blank string. Since I have {skip: searchboxText === ""}, the "data" doesn't reset to blank. As soon as the user goes to use the searchbox again it immediately shows the old data from the previously entered search term. Not sure if this is helpful but here is rough code on how i'm using it
The only viable workaround I found is to use
This tricks the hook into resetting the data of the hook to null when searchbox is empty. Unfortunately the type-safety isn't ideal. I thought of using I also tried using resetApiState directly after a result is selected. This doesn't reset the hook state. This aligns with what the docs say: "Note that hooks also track state in local component state and might not fully be reset by resetApiState." I can provide a reproduction if necessary but figure this is already an acknowledged behavior as shown in the docs. |
|
@xjamundx can you give more examples of each of those? For the "migration" aspect, does https://redux.js.org/usage/migrating-to-modern-redux#data-fetching-with-rtk-query help at all? What info would be more useful here? What aspects about the "interfacing" are confusing? |
I have a basic cursor paged query set up using a custom My problem is when trying to use tag invalidation. Say a mutation modifies this paged list by adding or removing an item. This invalidates the provided tag on the paged list (as it should) The problem is what happens next. Specifically if (querySubState.status !== QueryStatus.uninitialized) {
mwApi.dispatch(refetchQuery(querySubState, queryCacheKey))
} So what happens is that because there is already state in the cache, it calls the query again with the same arguments as the last call (include the pageToken). This just refetches the last page (and then What I want is to be able to detect when the query is being refetched as the result of tag invalidation (and not due to say useLazyQuery's However I have tried all different approaches and I am not able to isolate the refetch case. Using EDIT: After looking at the above further, I can see that it is not possible to detect duplicate requests in the EDIT EDIT: Actually, because in my case the pageToken is stored in the Can you please advise if there is a way to solve this issue I am having? |
If you are interested in my code, this is the helper function we use for building paged endpoints and the helper function for building a hook to simplify fetching the next page from the endpoint /**
* Build a query for fetching paged data.
* By sharing the cache key between pages, and appending the new data to the old rather than overwriting it,
* the single cache entry will contain all the fetched pages of data.
*
* Most of the time you will not call the generated hooks for this endpoint directly, instead use `buildUsePagedQuery`
* to create a hook that will fetch the first page and provide a `fetchNextPage` function to fetch subsequent pages,
* abstracting away the handling of the pageToken.
*
* @param builder - The endpoint builder to use
* @param requestBuilder - A function that takes the query args and returns an AxiosRequestConfig for the request (url, params etc)
*
* @example
* endpoints: builder => ({
* getUsers: buildPagedQuery<User, UsersFilterParams>(builder, params => ({ url: baseUrl, params })),
* })
*
* const usePagedUsersQuery = buildUsePagedQuery(usersApi.endpoints.getUsers)
*
* const { data, isFetching, fetchNextPage, allFetched } = usePagedUsersQuery()
*
* @returns The query definition
*/
export const buildPagedQuery = <DataType, QueryArgType = void>(
builder: AxiosEndpointBuilder,
requestBuilder: (params: Omit<WithPageArgs<QueryArgType>, 'pageToken' | 'limit'>) => AxiosRequestConfig,
extraOptions?: AxiosQueryEndpointExtraOptions<PagedData<DataType>>
) =>
builder.query<PagedData<DataType>, WithPageArgs<QueryArgType>>({
query: ({ pageToken, limit, ...params }) => {
const requestConfig = requestBuilder(params)
return {
...requestConfig,
params: { limit, pageToken, ...requestConfig.params },
}
},
serializeQueryArgs: endpoint => {
return defaultSerializeQueryArgs({
...endpoint,
// Don't serialize the pageToken or limit into the cache key so subsequent pages are added to the existing cache entry
queryArgs: omit(endpoint.queryArgs, 'pageToken', 'limit'),
})
},
merge: (currentData, nextPageData, args) => {
if (!Array.isArray(currentData.data)) {
currentData.data = []
}
if (Array.isArray(nextPageData.data)) {
currentData.data.push(...nextPageData.data)
}
currentData.pageToken = nextPageData.pageToken
},
...extraOptions,
})
/**
* A function that can be used to build a query hook that will fetch the first page of results from a paged endpoint
* (i.e. one created with `buildPagedQuery`) and provide a `fetchNextPage` function to fetch the next page of results.
*
* `fetchNextPage` will not attempt to fetch any more pages if the last page has already been fetched.
* (i.e. if the `pageToken` returned by the last request was `null`).
* An additional `allFetched` boolean is provided to indicate if all pages have been fetched.
*
* @example
* const usePagedUsersQuery = buildUsePagedQuery(usersApi.endpoints.getUsers)
*
* const { data, isFetching, fetchNextPage, allFetched } = usePagedUsersQuery()
*
* <Button disabled={isFetching || allFetched} onClick={fetchNextPage}>Load more</Button>
*/
export const buildUsePagedQuery =
<DataType>(pagedEndpoint: AxiosQueryEndpoint<PagedData<DataType>>) =>
(params = {}): PagedQueryState<DataType> => {
const useQueryResponse = pagedEndpoint.useQuery(params)
const pageToken = useQueryResponse.data?.pageToken
// Strict equality check to ensure that pageToken is null and not undefined
// (which would indicate that the query has not been run yet)
const allFetched = pageToken === null
const [triggerQuery] = pagedEndpoint.useLazyQuery()
const requestInFlight = useQueryResponse.isLoading || useQueryResponse.isFetching
const fetchNextPage = useCallback(() => {
if (!allFetched && !requestInFlight) {
triggerQuery({ pageToken, ...params })
}
}, [allFetched, requestInFlight, triggerQuery, pageToken, params])
return {
...useQueryResponse,
data: useQueryResponse.data?.data, // Only return the data, not the pageToken
currentData: useQueryResponse.currentData?.data,
fetchNextPage,
allFetched,
}
} |
So I have a solution for the above largely working, except that looking up the existing cache state in If I could ask one thing, it would be to make That would remove the need to know the reducerPath and cache key in Thanks |
Another use-case for @eric-crowell's "reactify" an api idea, is that many times I want to use the useQuery hooks in my components, but other times I need to refetch an api query in one of my sagas, or run a mutation in a saga that breaks the query cache, etc. |
@padge what's preventing you from doing that currently? the version of createApi from query/react still has everything that the version from query has. |
@EskiMojo14 my mistake, I somehow missed the "Usage Without React Hooks" page and |
@micmcg Thank you for sharing your code. I implemented it with some minor changes and encountered the exact issue you described above. The only solution I can think of is to create a fetchFirstPage callback in buildUsePagedQuery and use this callback to invalidate the cache where needed. It's not a perfect solution, but I can't see any other alternatives. |
Hi! Also, we only use code-generation, and would love to specifiy global options in |
@Richard87 looks like you edited the message, but to answer your earlier question: mutation triggers are standard RTK |
One of the biggest pain points I have encountered is dealing with the fact that the Example: function ComponentA() {
const { data, isLoading, isError } = useMyQuery();
if (isLoading) { return <p>Loading...</p> }
return (
<div>
<h3>{isError ? "Error" : data.title}</h3>
<ComponentB />
</div>
);
}
function ComponentB() {
const { data, error } = useMyQuery();
if (error) { return <p>{error.message}</p> }
return (<p>{data.message}</p>);
} |
I've actually experienced more or less the inverse of that as a problem: Basically, it means you need to either always use |
@rjgotten this is documented:
|
Documented behavior or not - it's a pain point. It does not firmly state that I'd also argue that the behavior as-is, is strange. Basically, if considering the query as a promise I would expect Orthogonality and principle of least surprise apply. |
Imagine a UI that displays a list of elements, currently on page 1. You navigate to page 2, so page 1 stays in view, but grays out (to prevent the UI from jumping around). Now you encounter an error. Something like that would prevent a janky UI jumping from one state into another and creating a lot of motion - and it's only possible if the last successful value stays available to you in some way. |
Imagine the following pattern: I have hooks such as I have a There is a However, I don't want to trigger this query if the post has already been fetched by one of: I can't really use I think I could use One thing I could do is provide tags from the results of those queries, such as:
I would just need a way to link the Essentially, RTK-Query uses a document cache by default, but via tags I think users could have a handy way to manually create normalized relationships between the different queries by processing the data results in |
@amirghafouri Hook into You will probably also need to make sure that |
Dependant queries, infinite queries and pseudo-normalization would be the most useful features in my opinion. It would remove a lot of solutions that seem "hacky". For infinite queries, I think it's important to also have refetching. If I have an infinite query with the first 75 items loaded, I should be able to refetch the 75 items with one call. With the pseudo-normalization, it could eliminate a lot of bridging between a slice and RTKQ IMO. |
Current painpoint I have is trying to infer the endpointName/querykey for the first parameter of updateQuerydata and I am getting a typescript error. im trying to use @rtk-query/codegen-openapi to generate the queries and while trying to perform an optimistic endpoint. So if the generated useQuery is useGetEventTypesQuery() then im trying to grab the name off of it and end up with useGetEventTypesQuery.name or alternatively get it via enhancedApi.endpoints.getEventTypes.name snippet
ts error
Is there a way to work around this? to programmatically get the key instead of having to type the string out, this would allow us to rely on our generated code from the api contract as consts as opposed to strings that someone could overwrite accidentally. well thats my general thoughts and hopes. if im just flat out not thinking about it in the rtk query way, that could be it too since im newer to using rtk query |
we could possibly strongly type the .name property without needing to make too many changes (#4332) but i would question how useful that is if to get it you already need to know the endpoint name you can't get it from a hook though, unless we added an extra property to the function - the fn.name property is built in so i don't think we should touch that |
Hello guys, I see that the doc mentions we should have a single API slice. What would happen if I used multiple API slices instead? I already have an app that implements multiple API slices based on specific biz features without any issues. |
@viper4595 I also split rtk-queries into different slices, using the approach described in this doc: https://redux-toolkit.js.org/rtk-query/usage/code-splitting |
@viper4595 you lose out on features like tag invalidation as those don't work over multiple slices @isqua I wouldn't really use that wording there - you still have one slice, it's just split onto multiple files :) |
Thank you, @phryneas! Please correct me if I'm wrong, but there's no performance impact with multiple slices, right? |
@viper4595 there is, because each slice has its own middleware, so dispatch becomes many more functions to execute. |
Yeah, every middleware gets called for every dispatched action. Having a handful of middleware is okay, but if you start adding a bunch the perf overhead will add up. At one point the RTKQ middleware was itself a composition of a half-dozen smaller middleware, and then people added lots of RTKQ APIs and started having stack overflow issues. We did do some optimization work to keep the RTKQ middleware flatter, but still, you should normally only have the one API slice and middleware by default. |
My painpoint: Having endpoint names clash. In a large application where multiple teams work on different APIs (created through injectEndpoints), there can be hard to debug problems when endpoint names clash. This is also discussed here #3350, but those warnings and throws only happen in runtime when both injectEndpoints get loaded.
but internally it would get I also don't want to create separate API slices (mentioned above), because when there are many, then the performance takes a hit |
@HarlesPilter not exactly what you're asking for, but you know that there are also non-"named" hooks? So you could do |
It feels like the RTK Query API and options have a lot of rough edges (especially the goofiness around people trying to hack together infinite queries because we don't have anything built in for that right now, per #3174 and #1163 ), but I don't have more specifics in my head right now.
Please comment here to provide feedback about anything that annoys you with RTK Query's current API design and behavior!
Some items I know have been brought up:
cacheEntryRemoved/cacheDataLoaded
promises, but you have to listen to both of them to do afinally {}
equivalentsomeApi/fulfilled
methods and not know the endpoint names (because it's buried in themeta
field)The text was updated successfully, but these errors were encountered: