javascriptreactjsreact-reduxredux-toolkitrtk-query

How to use parametrized queries in selectors?


I'm working on a React app with Redux-Toolkit (RTK) Query. I need to fetch accounts from the server and then render them as well derive some data from them through a chain of selectors. If I fetch them with createAsyncThunk and save in the store manually I can easily use them in any selector. I expected similar experience if I'm using RTK Query can't figure out how it works.

If I have a query without any parameters it works nicely:

export const apiSlice = createApi({
  reducerPath: 'API',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    getAccounts: builder.query<Account[], void>({
      query: () => 'accounts',
    }),
  }),
});

export const getAccountsQuery = apiSlice.endpoints.getAccounts.select();

export const getAccountsResult = createSelector(
  [getAccountsQuery],
  (accountsResult) => {
    return accountsResult.data || [];
  }
);

I can now use getAccountsResult in any selector. However if the endpoint requires a parameter it's a different story:

export const apiSlice = createApi({
  reducerPath: 'API',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    getAccounts: builder.query<Account[], void>({
      query: (orgId) => ({
        url: 'accounts',
        params: { orgId },
      }),
    }),
  }),
});

Now I need to pass orgId to apiSlice.endpoints.getAccounts.select() and I can't do it inside a selector. Ideally I'd like it to be something like:

export const getAccountsResult = createSelector(
  [getOrgId],
  (orgId) => {
    return apiSlice.endpoints.getAccounts.select(orgId).data || [];
  }
);

but that's not how it works. Is there a way to achieve it?

And more broadly speaking, am I even supposed to be use RTK Query this way? There is a lot of resources on how to access and control data inside components but there's surprisingly little info on how to work with data in selectors.


Solution

  • apiSlice.endpoints.getAccounts.select(orgId) creates a new selector function each time it's called, so it doesn't make for good use inline when trying to create a memoized selector function via createSelector. You can instantiate a memoized selector function using React's useMemo though, and use this in your useSelector/useAppSelector hook.

    Example:

    const selectAccountsByOrgId = React.useMemo(
      () => apiSlice.endpoints.getAccounts.select(orgId),
      [orgId],
    );
    
    const { data, ...rest } = useAppSelector(selectAccountsByOrgId);
    

    You might not actually need to do that though when you could just use your generated useGetAccountsQuery hook.

    const { data, ...rest } = useGetAccountsQuery(orgId, /* options */);
    

    Just make sure you are actually exporting the generated hooks:

    import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
    
    export const apiSlice = createApi({
      reducerPath: 'API',
      baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
      endpoints: (builder) => ({
        getAccounts: builder.query<Account[], string>({
          query: (orgId) => ({
            url: 'accounts',
            params: { orgId },
          }),
        }),
      }),
    });
    
    export const { useGetAccountsQuery } = apiSlice;
    
    ...
    

    Since you can't use these as input selectors in createSelector you can abstract the logic into a custom hook if necessary.

    Example:

    const useQueryComplexAccountByOrgId = (orgId: string) => {
      const selectAccountsByOrgId = React.useMemo(
        () => apiSlice.endpoints.getAccounts.select(orgId),
        [orgId],
      );
    
      const { data, ...rest } = useAppSelector(selectAccountsByOrgId);
      // OR
      // const { data, ...rest } = useGetAccountsQuery(orgId, /* options */);
    
      const otherSelectedStateValue = useAppSelector(selectComplexState);
      ...
    
      // ... business logic
    };
    
    const result = useQueryComplexAccountByOrgId(orgId);