redux-toolkit icon indicating copy to clipboard operation
redux-toolkit copied to clipboard

[RED-14] Using RTK query with multiple parameters retrieved from redux

Open kplatis opened this issue 2 years ago • 13 comments

Hello there. I have recently started using RTK query to fetch and cache my data in my frontend and I have some questions. My use case is the following:

  • I have some filters on my app
  • When a filter changes, a new request is executed to fetch resources
  • The current selection of the filters are stored in redux
  • For each filter, more than one values might be selected and as a result each one of them is an array of selected values.
  • I want to use RTK in order to cache the responses for each selection of filters
  • I use queryFn instead of query since I fetch data in a custom way

The api is defined as following:

    export const resourcesApi = createApi({
    	reducerPath: 'resourcesApi',
    	endpoints: (builder) => ({
    		getResources: builder.query({
    			queryFn: ({selectedFilter1, selectedFilter2}) => {
                            // this function fetches the data
                		return getResourcesBasedOnFilters(selectedFilter1, selectedFilter2)
    		},
    		}),
    	}),
    })
    
    export const { useGetResourcesQuery } = resourcesApi

And I call it using:

export default function Resources(props){
    let selectedFilter1 = useSelector((state) => state.filters.selectedFilter1)
    let selectedFilter2 = useSelector((state) => state.filters.selectedFilter2)
    
    const {isLoading, isFetching, data} = useGetResourcesQuery({selectedFilter1, selectedFilter2})

    if (isLoading){
        // show loader
    }
    else{
        // show data
    }
}
My questions are the following: 
  • The more filters I have, the slower redux becomes. Can I do something for it?
  • Can I access the filters directly in queryFn ?

RED-14

kplatis avatar Jun 29 '22 13:06 kplatis

@kplatis Can you give an example of what you mean by "the more filters I have, the slower Redux becomes"? Ideally as a runnable CodeSandbox or a repo, with some pointers to what's happening.

markerikson avatar Jun 29 '22 14:06 markerikson

Can I access the filters directly in queryFn ?

You should not, but go through the arguments - otherways we would not know when arguments update and need a new request to be made.

phryneas avatar Jun 29 '22 18:06 phryneas

Hello, answering to your questions:

  • The more filters I have, the slower redux becomes. Can I do something for it?
  • Can I access the filters directly in queryFn ?

For filters, you can get them this way, its way simple:

 const [filterOne, filterTwo] = useSelector((state) => state.filters):

you can pass the whole array as arg to use useQuery hook, like this

const {isLoading, isFetching, data} = useGetResourcesQuery(filters);

As for your question, about accessing filters directly inside queryFn, based on docs there is a second argument in queryFn(), called api, it contains getState(), so you can do:

getResources: builder.query({
   	queryFn: (filters, {getState} ) => {
               return getResourcesBasedOnFilters(getState().filter.selectedFilter1, getState().filter.selectedFilter2)
   	},
}),

fregayeg avatar Jul 03 '22 02:07 fregayeg

With this solution:

As for your question, about accessing filters directly inside queryFn, based on docs there is a second argument in queryFn(), called api, it contains getState(), so you can do:

getResources: builder.query({
   	queryFn: (filters, {getState} ) => {
               return getResourcesBasedOnFilters(getState().filter.selectedFilter1, getState().filter.selectedFilter2)
   	},
}),

It won't refetch when the filters change right?

  • When a filter changes, a new request is executed to fetch resources

It would be really nice to have some feature in the library to define dependencies like this on the API instead of in the hook. The component doesn't technically need to know anything about the "base" params of a fetch.

GriffinSauce avatar Jul 18 '22 05:07 GriffinSauce

To clarify, the idea from my previous comment could look something like this:

getResources: builder.query({
  selectFromState: (state) => {
    // Use some selectors here
  },
  queryFn: (args, {selection} ) => {
    return getResourcesBasedOnFilters(selection.filters)
  },
})

Basically subscribing the endpoint to Redux state directly instead of having to go through the component. RTKQ should merge args and selection for the cache key.

It might even be a good idea to use createStructuredSelector here:

getResources: builder.query({
  selectFromState: createStructuredSelector({
    filters: getFilters,
  }),
  queryFn: (args, {selection} ) => {
    return getResourcesBasedOnFilters(selection.filters)
  },
})

For me this would be very helpful to work with app-wide state like auth, landing parameters, global filters.

GriffinSauce avatar Jul 26 '22 14:07 GriffinSauce

Honestly, I am pondering about an API like this for a year now - and it would be a consequential next step.

It would add two levels of complexity:

  1. a significant level of type complexity
  2. the requirement to track those selectors and listen for state changes

We could avoid 1. by using an api like

getResources: builder.query({
  // could also be used in `query`
  queryFn: (args, {select} ) => {
    const filters = select(getFilters)
    return getResourcesBasedOnFilters(filters)
  },
})

instead - that would not add any TypeScript overhead.

But from that point, we would have to even track multiple selectors. Nontheless, I think this one would be the way to go.

The bummer is: at the moment I don't have the time to implement anything like that. It's just too much going on for an endeavor of that size.

phryneas avatar Jul 26 '22 18:07 phryneas

We only want to refetch when relevant state changes. I was assuming that the selection would need to be outside queryFn (or query) to be able to track and trigger new fetches when needed. How would that work with adding select to the api param? (I may be missing some context about how things work under the hood)

Thanks for considering, perhaps there are "lighter" solutions, I'm just shooting from the hip from the outside here.

GriffinSauce avatar Jul 29 '22 19:07 GriffinSauce

@GriffinSauce : one option, if perhaps a bit unwieldy, would be to add a listener middleware entry that watches the relevant bits of state using the predicate option, and specifically triggers refetching

markerikson avatar Jul 29 '22 20:07 markerikson

It won't refetch when the filters change right?

Just for clarity and to answer your question @GriffinSauce: It will not refetch if the filters are changed, and as I see it you also lose the convenience of the skip logic here when you implement the query using queryFn.

This concept of adding dependencies/selectors to the endpoints would be a great addition to the library! I, unfortunately, have no clue on where to begin in implementing something like it 😅

Vanluren avatar Oct 06 '22 09:10 Vanluren

@Vanluren filters in that case was the argument passed in by the hooks and that will definitely cause a refetch (unless data for those new filters is already in the cache)

phryneas avatar Oct 06 '22 10:10 phryneas

I'm a bit confused by the last comments @phryneas - we can already pass filters through arguments. Maybe we're missing some context here about your solution?

It will not refetch if the filters are changed

This is specifically what I'd like to have: responding to state changes without wiring it through the hook arguments.

Thanks for the comments to far 🙏

GriffinSauce avatar Oct 19 '22 09:10 GriffinSauce

Glad I found this! I have been spinning my wheels for a while trying to figure out how to access redux state (via a selector) either IN the query method or have it passed down to it from a custom base query via meta or extraOptions but I have not been successful. It seems like what I want is not possible. I want to second @GriffinSauce 's idea to access state variables within the query method.

An example of my use case would be something simple like this:

query: (arg, { getState }) => {
        const state = getState();
        const userName = selectUserName(state);
        // Option 1: return `/some/path/?&userName=${userName}`


        // Option 2 return {
          url: '/some/path/',
          params: { username: userName },
        };
      },

I know this is not 100% correct but something to this effect. I currently have to select userName from the store in every component where I need to pass userName as an argument to a query hook. It's starting to be duplicated a lot more than I'd like and I thought it would be nice to be able to just grab it at the endpoint definition.

THEaustinlopez avatar Sep 16 '23 06:09 THEaustinlopez