search

Home  >  Q&A  >  body text

How to use RTK queries with selectors

I'm building a project using Redux Toolkit and RTK Query and trying to get some entries from the API. I'm using the normalize data method of createEntityAdapter because in a certain component I need the data as an array, so I ended up using a selector. Now my problem is that since I added the filter as a parameter of the query, my selector stopped working.

I looked into similar questions here like: "How to use RTK query selector with parameters?", but I'm too stupid to understand what I should modify. I'm trying to understand the RTK query documentation, but I can't.

From the question above, I know that my selector also needs to have parameters in order to know exactly what to select, and that this is not a recommended pattern, but I can't understand how to make it work.

My entry slice:

import { createSelector, createEntityAdapter } from '@reduxjs/toolkit'
import { apiSlice } from './apiSlice'

const entryAdapter = createEntityAdapter()

const initialState = entryAdapter.getInitialState({
  ids: [],
  entities: {},
})

export const entryApiSlice = apiSlice.injectEndpoints({
  endpoints: (builder) => ({
    initialState,
    getEntry: builder.query({
      query: (filters) => ({
        url: '/history',
        params: filters,
      }),
      transformResponse: (responseData) => {
        return entryAdapter.setAll(initialState, responseData)
      },
      providesTags: (result, error, arg) => [
        { type: 'Entry', id: 'LIST' },
        ...result.ids.map((id) => ({ type: 'Entry', id })),
      ],
    }),
    addEntry: builder.mutation({
      query: (data) => ({
        url: '/history/new',
        method: 'POST',
        body: data,
      }),
      invalidatesTags: [{ type: 'Entry', id: 'LIST' }],
    }),
    updateEntry: builder.mutation({
      query: (initialEntry) => ({
        url: `/history/${initialEntry.Id}`,
        method: 'PUT',
        body: {
          ...initialEntry,
          date: new Date().toISOString(),
        },
      }),
      invalidatesTags: (result, error, arg) => [{ type: 'Entry', id: arg.id }],
    }),
    deleteEntry: builder.mutation({
      query: ({ id }) => ({
        url: `/history/${id}`,
        method: 'DELETE',
        body: { id },
      }),
      invalidatesTags: (result, error, arg) => [{ type: 'Entry', id: arg.id }],
    }),
  }),
})

export const {
  useGetEntryQuery,
  useAddEntryMutation,
  useUpdateEntryMutation,
  useDeleteEntryMutation,
} = entryApiSlice

export const selectEntryResult = (state, params) =>
  entryApiSlice.endpoints.getEntry.select(params)(state).data

const entrySelectors = entryAdapter.getSelectors(
  (state) => selectEntryResult(state) ?? initialState
)
export const selectEntry = entrySelectors.selectAll

I use this in an Entries component like this

const {
    data: entriesData = [],
    refetch,
    isLoading,
    isSuccess,
    isError,
    error,
  } = useGetEntryQuery(filters)

  const entries = useSelector(selectEntry)

Note: If I remove the "filter" from the get query, everything works as before (as expected, of course).

Disclaimer: I have no idea what the hell I'm doing, I've read the documentation and am trying to figure it out, so any feedback is greatly appreciated. Thanks!

P粉729198207P粉729198207331 days ago676

reply all(1)I'll reply

  • P粉779565855

    P粉7795658552023-12-29 00:13:52

    Yes, this is a somewhat sensitive topic, as RTKQ's documentation tends to show the simplest examples, queries that don't use any parameters at all. I've had a lot of problems myself.

    Anyway, you have declared selectEntryResult as a function with two parameters: state and params. Then, when you create the adapter selector underneath it, you call it with only one parameter: state. Also, when you use the final selector in your component like this:

    const entries = useSelector(selectEntry);
    
    The

    parameters are nowhere to be found, they are undefined by default and no data associated with such query parameters can be found.

    The key thing to understand here is that you actually need to pass the query parameters through each level of the selector (each wrapper) somehow.

    One way here is to "forward" the parameters through the selector:

    export const selectEntryResult = createSelector([state => state, (_, params) => params], (state, params) =>
      entryApiSlice.endpoints.getEntry.select(params)(state)?.data ?? initialState)
    

    Here we use the createSelector function exported from RTK. Then in your component you would do something like this:

      const {...} = useGetEntryQuery(filters);
    
      const entries = useSelector(state => selectEntry(state, filters));
    

    This works when using the selectAll selector created by the entity adapter, but causes problems when using selectById since that selector is also parameterized. In short, the selectById selector is internally defined to take a second argument of the entity id you wish to retrieve, whereas the method I showed uses the second argument to pass the query parameters (your filter filter in ). case).

    As far as I know, there is no solution so far that works perfectly and covers all of the following:

    • Use entity normalization
    • Retrieve data using selectors
    • Use parameterized queries

    Another approach might be to create some selector factories that dynamically create base selectors for specific combinations of query parameters.

    I once made such a wrapper that can be used in all situations. Unfortunately I can't share it because it's a private package, but the basic idea is to change the parameters so that both selectById and selectAll (and all other selectors) work correctly , by passing the query parameters as the third parameter to the selector, and then further rewrapping each entity adapter selector:

    export const createBaseSelector = (endpoint) =>
      createSelector(
        [(state) => state, (_, other) => other, (_, _other, params) => params],
        (state, _other, params) => endpoint.select(params)(state)
      );
    
    const selectors = adapter.getSelectors(baseSelector);
    
    // And then rewrap selectAll and selectById from 'selectors' above
    

    I know this sounds complicated and I've barely gotten it to work, so try to avoid going in this direction :)

    A helpful article I found along the way can be found here They also describe some ways of creating selectors at the component level and remembering them, but I haven't tried them all yet. Have a look, maybe you'll find an easier way to solve your specific problem.

    reply
    0
  • Cancelreply