Skip to content
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

Open
markerikson opened this issue Sep 1, 2023 · 101 comments
Open

RTK Query API pain points and rough spots feedback thread #3692

markerikson opened this issue Sep 1, 2023 · 101 comments
Milestone

Comments

@markerikson
Copy link
Collaborator

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:

  • The cache lifecycle methods do not run when there's a cache hit
  • There's a couple different cacheEntryRemoved/cacheDataLoaded promises, but you have to listen to both of them to do a finally {} equivalent
  • Kind of annoying to see all the someApi/fulfilled methods and not know the endpoint names (because it's buried in the meta field)
@markerikson markerikson added this to the 2.0 milestone Sep 1, 2023
@Dovakeidy
Copy link

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 .
The result is infinite scrolling, but with an extra load for each request. However, it worked well in my case, given that there will never be thousands of data items.

Another very minor point is the data property on queries:

const { data, error, isLoading } = useGetPokemonByNameQuery('bulbasaur');

Every time I have to rename data:

const { data: pokemons, error, isLoading } = useGetPokemonByNameQuery('bulbasaur');

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.

const [pokemons, { error, isLoading } ] = useGetPokemonByNameQuery('bulbasaur');

Right now I can't think of anything else, but I'll be back if I ever do!

@eric-crowell
Copy link
Contributor

eric-crowell commented Sep 2, 2023

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>
}

@eric-crowell
Copy link
Contributor

eric-crowell commented Sep 3, 2023

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 Example

import { 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 Thoughts

Honestly, 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.

@nhayfield
Copy link

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.
It is also not easy to do other custom things like access response headers from the generated file. It seems like generating from open api spec is actually a mistake and you really need to just write a bunch of code manually to use any of the good features.

Maybe I am wrong about some of this. Feel free to correct me.

@markerikson
Copy link
Collaborator Author

@nhayfield can you give a couple further details?

  • What do you mean by "normalize the endpoints"?
  • What's an example of "consolidating multiple calls"?
  • Where and how would you want to access response headers?

@nhayfield
Copy link

nhayfield commented Sep 6, 2023

@nhayfield can you give a couple further details?

  • What do you mean by "normalize the endpoints"?
  • What's an example of "consolidating multiple calls"?
  • Where and how would you want to access response headers?

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

@markerikson
Copy link
Collaborator Author

@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 transformResponse.

Where and how would you want to "chain calls together"?

Where and how would you want to access response headers, in what code?

@jarvelov
Copy link

jarvelov commented Sep 7, 2023

Just wanted to chime in and express support for the proposal of unifying the API by @Dovakeidy:

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.

const [pokemons, { error, isLoading } ] = useGetPokemonByNameQuery('bulbasaur');

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.

@nhayfield
Copy link

@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 transformResponse.

Where and how would you want to "chain calls together"?

Where and how would you want to access response headers, in what code?

#3506
#3485
these are the issues, regarding the issues with enhanceEndpoints, transformResponse, and typescript.
it doesn't seem like there has been any movement on the PR that addresses the issue.

const { data, error, isLoading } = useGetPokemonByNameQuery('bulbasaur');
this is an example of the type of generated hook i am wanting to access the Response Headers from. It doesn't appear possible.

@markerikson
Copy link
Collaborator Author

markerikson commented Sep 7, 2023

@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 {data, isLoading, headers} ?

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 {data} or {error}. None of that is HTTP-specific. It's fetchBaseQuery that makes an HTTP request specifically. So, conceptually, headers don't fit into the output format of a query hook, because nothing about that result relates to HTTP at all.

I think you might want to try writing a custom version of fetchBaseQuery that includes the headers as part of whatever actual data value was fetched, so that they'll get saved into the cache entry:

@nhayfield
Copy link

@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 {data, isLoading, headers} ?

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 {data} or {error}. None of that is HTTP-specific. It's fetchBaseQuery that makes an HTTP request specifically. So, conceptually, headers don't fit into the output format of a query hook, because nothing about that result relates to HTTP at all.

I think you might want to try writing a custom version of fetchBaseQuery that includes the headers as part of whatever actual data value was fetched, so that they'll get saved into the cache entry:

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.

@rwilliams3088
Copy link

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

@markerikson
Copy link
Collaborator Author

@rwilliams3088 can you clarify what you mean by "lack of official support for complex objects? What's an example of that?

@rwilliams3088
Copy link

rwilliams3088 commented Sep 11, 2023

@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 reduxify 💯

@rwilliams3088
Copy link

rwilliams3088 commented Sep 11, 2023

Another, smaller request for efficiency: drop uneccessary state like result and lastPromiseInfo from the useLazyQuery hook. Since trigger returns a promise with all the request and response details, and since I'm often sending many simultaneous requests, that state holds little value to me. Furthermore, it means that each time I send a request there are unnecessary re-renders of the component using the hook.

@Dovakeidy
Copy link

@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 ?

@seanmcquaid
Copy link

seanmcquaid commented Sep 11, 2023

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.

@markerikson
Copy link
Collaborator Author

@seanmcquaid What's the benefit of having callbacks as opposed to using await doSomeMutation()?

(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 )

@seanmcquaid
Copy link

@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!

@rwilliams3088
Copy link

rwilliams3088 commented Sep 11, 2023

@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 ?

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.

@mjwvb
Copy link

mjwvb commented Sep 12, 2023

I'm a very happy user of RTK query for a very data intensive desktop app. Some feedback off the top of my head:

  • Invalidate current cache entry from within onCacheEntryAdded (e.g. invalidateCachedData, just like updateCachedData). Right now we use some tag invalidation trickery for that.
  • Invalidation of long running queries (Invalidating long running initial GET requests. #2215). Say you have a long query of 10 seconds and you invalidate during that request, it won't cancel that request and won't trigger a new request.
  • Ability to cancel queries in general.
  • Maybe an easier way of passing extra options/parameters to a query without making it part of the query args. Since serializeQueryArgs it is possible to omit arguments from serializing, but it's a little bit cumbersome.

@markerikson
Copy link
Collaborator Author

markerikson commented Sep 12, 2023

@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?

@mjwvb
Copy link

mjwvb commented Sep 12, 2023

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.

@mjwvb
Copy link

mjwvb commented Sep 12, 2023

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.

@markerikson
Copy link
Collaborator Author

markerikson commented Sep 18, 2023

Tossing out a few things that I know have come up a number of times:

  • There's no immediate way to do "dependent queries" via just the core API. The only real workaround is a custom queryFn that dispatches the first request thunk, and then use that result for the second query. Conceptually, this feels sort of like allowing queries to invalidate tags too?
  • We currently implement the React hooks behavior by having an entire second implementation of the RTKQ createApi method with the hooks logic mixed in. It would be nice if we could do it as more of a layer on top of the core API. In other words, create a UI-less api instance first, then add the hooks on top of that. The basic public usage would still be the same, just import { createApi } from "@reduxjs/toolkit/query/react", but this would allow generating a UI-less api instance for use on the server side, and then extending it with the React hooks on the client side. It might also simplify other UI integrations as well. (Conceptually, I can almost envision it as a form of injectEndpoints - call methods on the original API instance, return the same API instance but with new capabilities built in and updated TS types?)
  • Infinite queries ala Implementing infinite scroll with RTK Query #1163 and RTKQ "infinite query" potential use cases and concerns #3174
  • A pseudo-normalization approach. If you fetch a list of items, it would be neat to also add those to the store as individual entries as well (so, getTodos() could also save items as getTodo(todoId)). I feel like that could help bridge the gap with some more complex use cases. (Some interesting discussion here: https://twitter.com/bancek/status/1703880605379784832 )

@codingedgar
Copy link

I struggle with cache and optimistic updates, specially when I have:

fetchAllOfX -> Saves in one cache
CrudOfX -> optimistic update/cache invalidation to fetchAll might not work because I cannot add tags while updating cache, so added entries are “imposible to invalidate”

wish I could normalize the cache or customise it in some way that allows to share ir between URLs

@agusterodin
Copy link

agusterodin commented Sep 19, 2023

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.mov

When 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


export default function TargetListFormSearchbox() {
  const dispatch = useDispatch()
  const searchboxText = useSelector(getTargetListFormSearchboxText)

  const debouncedSearchboxText = useDebouncedSearchboxText(searchboxText, 750)
  const { data: rawSearchResults } = useGetTargetListSearchResultsQuery(debouncedSearchboxText, {
    skip: searchboxText === null
  })
  const searchResults =
    searchboxText !== '' ? rawSearchResults?.map(searchResult => ({ id: String(searchResult.id), value: searchResult.name })) || null : null

  const inputRef = useRef<HTMLInputElement>(null)

  const form = useTargetListForm()

  return (
    <AutoCompleteSearchbox
      inputRef={inputRef}
      className="-ml-2.5 -mt-2 mb-2 w-[310px]"
      placeholder="Search existing target lists"
      results={searchResults}
      value={searchboxText}
      setValue={value => dispatch(setTargetListFormSearchboxText(value))}
      onResultSelected={result => {
        inputRef.current?.blur()
        dispatch(setTargetListFormSearchboxText(''))
        dispatch(fetchTargetList({ id: parseInt(result.id), targetListForm: form }))
      }}
      onClear={() => dispatch(setTargetListFormSearchboxText(''))}
    />
  )
}

The only viable workaround I found is to use queryFn

import { campaignsApi } from './index'

const targetListApiSlice = campaignsApi.injectEndpoints({
  endpoints: builder => ({
    getTargetListSearchResults: builder.query<null | TargetListSearchResult[], string>({
      queryFn: async (searchText, baseQueryApi, baseQuery, fetchWithBaseQuery) => {
        if (searchText === '') return { data: null }
        return (await fetchWithBaseQuery(`/targetList/search/${searchText}`)) as any
      },
      keepUnusedDataFor: 0
    })
  }),
})

export const { useGetTargetListSearchResultsQuery, endpoints: targetListEndpoints } = targetListApiSlice

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 currentData instead, but that would make it so the dropdown doesn't "smoothly" change between results (result dropdown will periodically disappear every time a character is typed)

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
Copy link

  1. This might be a bit out of scope, but I initially found it incredibly challenging to see how RTK Query interfaced with my redux store. Even now it feels very secret and separate and that makes migrations quite awkward. If you're starting from scratch and don't have pre-conceived notions of how redux works, it's fine, but if you're trying to adopt RTK Query into an existing redux application, it can be a bit weird. What's the takeaway here? IDK.
  2. graphql works, but it's poorly documented and arguably a bit convoluted. I initially tried it, got it working awkwardly then switched to urql, before the team pressured me to switch to apollo. I still like RTKQ best for many things, but a full effort to make graphql support first-class would be great. It could become a real player in graphql clients potentially.
  3. I'm still not convinced a full API-first approach is needed and that a simpler API might be possible that's more built around URLs rather than APIs. Just thinking about making getting starting easier.

@markerikson
Copy link
Collaborator Author

@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?

@leafty
Copy link

leafty commented Jan 24, 2024

One of the biggest pain points I have encountered is dealing with the fact that the useQuery hook will fetch data on mount when there was a previous useQuery hook which resulted in an error state. This has often created infinite render loops when two components use the same query and one is nested inside the other.

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>);
}

@rjgotten
Copy link

rjgotten commented Jan 24, 2024

One of the biggest pain points I have encountered is dealing with the fact that the useQuery hook will fetch data on mount when there was a previous useQuery hook which resulted in an error state.

I've actually experienced more or less the inverse of that as a problem:
if there's a useQuery hook subscribed; its args change and the query re-runs, but hits an error state, it retains the data from the last succesful query even though that had totally different args.

Basically, it means you need to either always use currentData instead of data and not care.
OR you need to pedantically check isError and error all the time.
(Humorous observation: it means you can have data and error at the same time. Bit of an identity crisis, no?)

@EskiMojo14
Copy link
Collaborator

@rjgotten this is documented:

  • data - The latest returned result regardless of hook arg, if present.
  • currentData - The latest returned result for the current hook arg, if present.

@rjgotten
Copy link

@EskiMojo14

Documented behavior or not - it's a pain point.
Also, I would argue that the documentation is not being very clear there.

It does not firmly state that data will hold the latest succesful returned result regardless of hook arg.

I'd also argue that the behavior as-is, is strange.
I would expect data to hold on to the last returned result regardless of hook arg, until any latest running query's finish running and if those fail for data to become undefined like currentData would.

Basically, if considering the query as a promise I would expect data (and for that matter maybe also error ?) to 'pin' their last settled values, whether resolved or rejected, while the promise is still pending, but to always accurately reflect the latest settled value, again: whether resolved or rejected.

Orthogonality and principle of least surprise apply.

@phryneas
Copy link
Member

phryneas commented Jan 24, 2024

I would expect data to hold on to the last returned result regardless of hook arg, until any latest running query's finish running and if those fail for data to become undefined like currentData would.

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.
You might want to present a pop-up with the error message and a "refetch" button, but at the same time keep the old (still grayed-out) data visible in the background. That data isn't wrong, it's just not current - and it's the last successful state your application has.

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.

@amirghafouri
Copy link

Imagine the following pattern:

I have hooks such as useFetchTrendingPostsQuery, useFetchFeaturedPostsQuery, useFetchTechnologyPostsQuery, etc... which all return different entities of the same type.

I have a Post component, which I want to accept only a postId prop.

There is a useFetchPostQuery which would accept a postId param and fetch a single post.

However, I don't want to trigger this query if the post has already been fetched by one of: useFetchTrendingPostsQuery, useFetchFeaturedPostsQuery, useFetchTechnologyPostsQuery.


I can't really use selectFromResult, because Post is a generic component and it would have to check all of the different api queries for different types of posts.

I think I could use extraReducers to put the results from the various query hooks for different types of posts into a single store slice called posts and check that, but that feels like it defats the point of having rtk-query?


One thing I could do is provide tags from the results of those queries, such as:

fetchFeaturedPosts: builder.query({
      providesTags: data => {
          return data.map(i => ({ type: 'post', id: i.id }));
      },
      query: fetchFeaturedPostsQuery
}),
fetchTrendingPosts: builder.query({
      providesTags: data => {
          return data.map(i => ({ type: 'post', id: i.id }));
      },
      query: fetchTrendingPostsQuery
}),
fetchPost: builder.query({
      providesTags: (data, error, arg) => {
          return [{ type: 'post', id: arg.postId }];
      },
      query: fetchPost
}),

I would just need a way to link the {type: 'post', id: postId } tag back to useFetchPost in a way that would make it avoid fetching that specific post if the postId matched.

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 providesTags.

@rjgotten
Copy link

rjgotten commented Feb 15, 2024

@amirghafouri
Use manual cache population.

Hook into onQueryStarted on each of your endpoints which fetches collections of posts, await queryFullfilled so you have the result, and then push that into a manually created or updated cache entry for fetchPost.

You will probably also need to make sure that fetchPost cache has a lifetime that doesn't immediately invalidate when it has no subscriptions.

@tibo-glamarche
Copy link

Tossing out a few things that I know have come up a number of times:

  • There's no immediate way to do "dependent queries" via just the core API. The only real workaround is a custom queryFn that dispatches the first request thunk, and then use that result for the second query. Conceptually, this feels sort of like allowing queries to invalidate tags too?
  • We currently implement the React hooks behavior by having an entire second implementation of the RTKQ createApi method with the hooks logic mixed in. It would be nice if we could do it as more of a layer on top of the core API. In other words, create a UI-less api instance first, then add the hooks on top of that. The basic public usage would still be the same, just import { createApi } from "@reduxjs/toolkit/query/react", but this would allow generating a UI-less api instance for use on the server side, and then extending it with the React hooks on the client side. It might also simplify other UI integrations as well. (Conceptually, I can almost envision it as a form of injectEndpoints - call methods on the original API instance, return the same API instance but with new capabilities built in and updated TS types?)
  • Infinite queries ala Implementing infinite scroll with RTK Query #1163 and RTKQ "infinite query" potential use cases and concerns #3174
  • A pseudo-normalization approach. If you fetch a list of items, it would be neat to also add those to the store as individual entries as well (so, getTodos() could also save items as getTodo(todoId)). I feel like that could help bridge the gap with some more complex use cases. (Some interesting discussion here: https://twitter.com/bancek/status/1703880605379784832 )

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.

@rcuching
Copy link

rcuching commented Apr 6, 2024

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
    const endpointName: string = enhancedApi.endpoints.getEventTypes.name
    // ts error
    const altEndpointName = useGetEventTypesQuery.name
    // works correctly
    const workingendpointName = 'getEventTypes'

    const tempPatch = dispatch(
      updateQueryData(endpointNamt, originalArgs!, (draft) => {
        draft.result = [...body.body];
      })
    );

ts error

TS2345: Argument of type string is not assignable to parameter of type
QueryKeys<{   getVersion: QueryDefinition<void, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, AppVersion, 'api'>;   ... 237 more ...;   getHours: QueryDefinition<...>; }>

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

@EskiMojo14
Copy link
Collaborator

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

@viper4595
Copy link

viper4595 commented Oct 30, 2024

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.

@isqua
Copy link
Contributor

isqua commented Oct 30, 2024

@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

@phryneas
Copy link
Member

phryneas commented Oct 30, 2024

@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 :)

@viper4595
Copy link

@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?

@EskiMojo14
Copy link
Collaborator

@viper4595 there is, because each slice has its own middleware, so dispatch becomes many more functions to execute.

@markerikson
Copy link
Collaborator Author

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.

@HarlesPilter
Copy link

HarlesPilter commented Nov 4, 2024

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.
I think when injectEndpoints would have a optional key parameter, that internally differentiates the endpoints. Something like

api.injectEndpoints({
  key: 'prefix',
  endpoints: (builder) => ({
    getData: ...
  })
});

const { useGetDataQuery } = api;

but internally it would get ${prefix}${endpointName}, so api.endpoints.endpointName would return 'prefixgetData'. I know this can be done by prefixing each endpoint name ourselves, but that might make the endpoint hooks quite long and hard to force when there are multiple teams working on their APIs. Would be nice if the library handles it endpoint isolation internally.

I also don't want to create separate API slices (mentioned above), because when there are many, then the performance takes a hit

@phryneas
Copy link
Member

phryneas commented Nov 4, 2024

@HarlesPilter not exactly what you're asking for, but you know that there are also non-"named" hooks?

So you could do api.veryLongEndpointName.useQuery via export const { veryLongEndpointName: shortName } = api and then shortName.useQuery.

@vasilii-kovalev
Copy link

vasilii-kovalev commented Dec 6, 2024

So you could do api.veryLongEndpointName.useQuery via export const { veryLongEndpointName: shortName } = api and then shortName.useQuery.

Thank you for this comment! A was thinking about asking for this feature, but it already exists. It is not possible to jump to the query/mutation definition and find all the usage places with auto-generated hook names, which is annoying.

@HarlesPilter
Copy link

HarlesPilter commented Dec 9, 2024

@HarlesPilter not exactly what you're asking for, but you know that there are also non-"named" hooks?

So you could do api.veryLongEndpointName.useQuery via export const { veryLongEndpointName: shortName } = api and then shortName.useQuery.

What I currently have done is create a wrapper around injectEndpoints. So instead of

api.injectEndpoints({ endpoints: ..., overrideExisting: false })

I do

createNamedApi('prefix', { endpoints: ..., overrideExisting: false })

What createNamedApi does is takes the 'prefix' and adds it to all the endpoint keys. getPosts vs getPrefixItems
and it returns the hooks where prefix is stripped from the hook name. useGetPostsQuery vs useGetItemsQuery. Internally the endpoint has the prefix in the name.
It was a pain to get it working with all the Typescript support but serves my needs.

Now we force the usage of createNamedApi and that requires the prefix to be set. Yes there can be two apis with the same prefix set, but I think it is easier to validate, that no same prefix exists in the codebase than to compare the endpoint names.

@Elieserlaguerre
Copy link

@viper4595 you lose out on features like tag invalidation as those don't work over multiple slices

Honestly, in my experience tag invalidations do not seem to work as well as I believe one should expect them to work after splitting your API code; I followed this documentation here: https://redux-toolkit.js.org/rtk-query/usage/code-splitting to split the API in my Next.js 14 app into multiple files because I'm building a multi-tenant app where each subdomain app's API is contained within a subfolder in the root API folder—all of the endpoints of my API work as expected, except for tag invalidations.

My expectation was that because I was injecting all of my endpoints into an empty root API file, as depicted by the code splitting documentation guide, I should have been able to invalidate certain query endpoints after a mutation, even if that mutation came from the endpoints of another file because they're part of the same API. for example, in my root API folder, I have two subfolders, /admin and /finance, the admin subfolder contains all of the API endpoints for the admin subdomain app that have already been injected into the empty root API file where a single createApi instance is being called, and the same goes for the finance subfolder. so, I have one API with multiple endpoints being injected from multiple separate files, as instructed by the guide. now, what I want to happen is, when a user in the finance subdomain app sends a message or submits a service request from their dashboard to the admin app, I want to invalidate certain query endpoints in the /admin API file, so I can automatically trigger a refetch to keep my cache data up to date, but that does not seem to work; even though I'm invalidating the tags from the /finance API endpoints, the /admin API endpoints do not respond. The only time tag invalidation works is when the invalidation occurs from endpoints that exist in the same file. I made sure to define all of my "tagTypes" in the empty root API file thinking that would make them available to all the other endpoints that would later be injected, but that does not seem to be the case. only endpoints that exist in the same files can invalidate each other's tags, but that is not what I need.

further research has led me to look into streaming updates to see if that could be a potential solution, but all of my research keeps pointing me toward having to implement a solution like Websocket.io, but that would require me to set up a separate server, and I don't want to go in that direction. I want to keep everything within the next.js framework if possible. so, my question is, is there a way to invalidate tags across various subdomain apps and their respective injected API files? I want the various apps that will exist in my multi-tenant app to be able to not only communicate with each other but also trigger events in one another, like tag invalidation for starters. if this feature already exists, then can someone please help me understand what I'm doing wrong and how to fix it? if this feature does not exist, will you please add it to the future releases of RTKQ?

@markerikson
Copy link
Collaborator Author

markerikson commented Dec 14, 2024

@Elieserlaguerre tag invalidation most definitely does work, and does work in code splitting / injected endpoint scenarios. If it doesn't seem to be working for you, could you file a separate issue with a link to a repo that shows this not working as expected?

(actually, now that I re-read your comment, I'm definitely confused by the "subdomain apps" description - I'd really appreciate an example project so I can understand how you've got things configured. My first vague guess is that some of the code split files are never getting loaded, and so there's nothing to invalidate, but I'd have to see the actual code in action to be sure.)

@Elieserlaguerre
Copy link

Elieserlaguerre commented Dec 20, 2024 via email

@markerikson
Copy link
Collaborator Author

@Elieserlaguerre

Could you post that as a new topic over in the "Discussions" section?

This thread is focused on specific problems or concerns people have with RTK Query's actual design that we could improve - it's easier to keep track of "how do I use this?" questions if they're in their own discussion threads.

@thorn0
Copy link
Contributor

thorn0 commented Dec 26, 2024

Would be good to see a reply to #3692 (comment). It's confusing and unexpected to pass skipToken and get irrelevant data in return.

Update: found #1492. Seems like this is considered an edge case that's not worth fixing

My current workaround
for (const [key, value] of Object.entries(api)) {
  if (typeof value === 'function' && /^use[A-Z].+Query$/.test(key)) {
    (api as any)[key] = (...args: any[]) => {
      const originalResult = (value as any)(...args);
      const arg = args[0];
      return useMemo(() => {
        if (arg !== skipToken || !originalResult.data) return originalResult;
        return { ...originalResult, data: undefined };
      }, [arg, originalResult]);
    };
  }
}

@markerikson
Copy link
Collaborator Author

@thorn0 I think you probably want to use the currentData field instead of data, as that should change to undefined when you pass in skipToken:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests