reduxjs / redux-toolkit

The official, opinionated, batteries-included toolset for efficient Redux development
https://redux-toolkit.js.org
MIT License
10.55k stars 1.13k forks source link

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

Open kplatis opened 2 years ago

kplatis commented 2 years ago

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:

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: 

RED-14

markerikson commented 2 years ago

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

phryneas commented 2 years ago

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.

fregayeg commented 2 years ago

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)
        },
}),
GriffinSauce commented 1 year ago

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 commented 1 year ago

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.

phryneas commented 1 year ago

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.

GriffinSauce commented 1 year ago

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.

markerikson commented 1 year ago

@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

Vanluren commented 1 year ago

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 😅

phryneas commented 1 year ago

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

GriffinSauce commented 1 year ago

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 🙏

THEaustinlopez commented 10 months ago

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.

PRO-GRAM-MER commented 1 month ago

i have a code but dont know how to fix it . Recently trying to use rtk query i need to dynamically change the select() params according to which my filter api will call and have data

import { apiSlice } from "./apiSlice";

const vrpListAdapter = createEntityAdapter({
  selectId: (vrp) => vrp.request_id,
});
const initialState = vrpListAdapter.getInitialState();

export const vrpListSlice = apiSlice.injectEndpoints({
  endpoints: (builder) => ({
    getVrpList: builder.query({
      query: ({ seller_id, status }) =>
        `/vrp?seller_id=${seller_id}&status=${status}`,
      transformResponse: (responseData) => {
        const loadedVrpList = responseData.data;
        console.log(loadedVrpList);
        return vrpListAdapter.setAll(initialState, loadedVrpList);
      },
      providesTags: (result, error, arg) => [
        { type: "Vrp", id: "vrpList" },
        ...result.ids.map((id) => ({ type: "Vrp", id })),
      ],
    }),
  }),
});

export const { useGetVrpListQuery } = vrpListSlice;

export const createSelectsVrpListResult = (seller_id, status) => {
  return vrpListSlice.endpoints.getVrpList.select({ seller_id, status });
};

export const selectsVrpListResult = (state) => {
  return createSelectsVrpListResult(
    state.vrpFilter.seller_id,
    state.vrpFilter.status
  )(state);
};

const selectVrpListData = createSelector(
  selectsVrpListResult,
  (vrpListResult) => vrpListResult.data
);

export const {
  selectAll: selectVrpList,
  selectById: selectVrpById,
  selectIds: selectVrpIds,
} = vrpListAdapter.getSelectors(
  (state) => selectVrpListData(state) ?? initialState
);

now everything is fine but i am getting a warning An input selector returned a different result when passed same arguments. This means your output selector will likely run more frequently than intended. Avoid returning a new reference inside your input selector, e.g. createSelector([state => state.todos.map(todo => todo.id)], todoIds => todoIds.length)

i am not able figure out how to use create selector i want to have seller_id and status which is there in vrpFilter reducer somehow i didi it but its not optimal please help!!!!!