typescriptredux-toolkitrtk-query

Can you use RTK Query queryFn with transformResponse and createEntityAdapter?


I'm pretty new to using adapters and RTK Query, and also not too advanced with TS, so sorry if the answer seems obvious! Everything is working fine when using queryFn with firebase and using the response data through my app, but I'm trying to use createEntityAdapter and keep running into TS issues and the overall logic not working correctly.

Here's the API I'm creating to retrieve all of the previous workouts from firestore. I've tried just returning what I have in the transformResponse method inside of queryFn, but that throws an error since it's expecting the result formatted similar to {data: data}. I've read in other places you can't use queryFn and transformResponse together since transformResponse is used to transform the data received from the query method and queryFn allows you to do this. Assuming there's an obvious approach with queryFn but I'm not knowledgable enough to see it.

export const workoutApi = createApi({
  baseQuery: fakeBaseQuery<WorkoutError>(),
  endpoints: (build) => ({
    getWorkouts: build.query<EntityState<Workout, string>, void>({
      queryFn: async () => {
        try {
          const workouts = await firestore().collection('workouts').get();
          const data = workouts.docs.map((doc) => ({
            id: doc.id,
            name: doc.data().name,
            date: format(doc.data().date.toDate(), 'MM/dd/yyyy'),
          }));

          return { data };
        } catch (e) {
          return {
            error: {
              status: 500,
              message: (e as Error).message,
            },
          };
        }
      },
      transformResponse(res: Workout[]) {
        return workoutsAdapter.setAll(initialState, res);
      },
    }),
  }),
});

I get 2 typescript errors:

The first is on the queryFn method:

Type '() => Promise<{ data: { id: string; name: any; date: string;
}[]; error?: undefined; } | { error: { status: number; message:
string; }; data?: undefined; }>' is not assignable to type '(arg:
void, api: BaseQueryApi, extraOptions: {}, baseQuery: (arg: void) =>
MaybePromise<QueryReturnValue<unique symbol, WorkoutError, {}>>) =>
MaybePromise<...>'.
    Type 'Promise<{ data: { id: string; name: any;
date: string; }[]; error?: undefined; } | { error: { status: number;
message: string; }; data?: undefined; }>' is not assignable to type
'MaybePromise<QueryReturnValue<EntityState<Workout, string>,
WorkoutError, {} | undefined>>'.

The second is on the transformResponse method:

Type '(res: Workout[]) => EntityState<Workout, string>' is not
assignable to type 'undefined'.

I might just be doing this completely wrong, but all of the examples I've found use the standard query method instead of queryFn.

Any help is really appreciated! Maybe it's just not possible using queryFn, but at this point I'm lost.


Solution

  • I've read in other places you can't use queryFn and transformResponse together since transformResponse is used to transform the data received from the query method and queryFn allows you to do this.

    This is correct. If you check the transformResponse documentation the first thing it states is:

    (optional, not applicable with queryFn)

    Trying to use transformResponse is not intended to work when using the queryFn.

    I think you are attempting to merge two completely separate concepts, i.e. RTK-Query caching query results vs Entity Adapters data structures for CRUD operations. The queryFn and its return value is simply used to return a value to the query cache. If you are also needing to update an entity adapter with the result value then I suspect that using onQueryStarted is what you are looking for. It's effectively an API endpoint "side-effect", e.g. when the query resolves you can issue side-effects to dispatch additional actions, or in your case update an entity adapter.

    Example:

    export const workoutApi = createApi({
      baseQuery: fakeBaseQuery<WorkoutError>(),
      endpoints: (build) => ({
        // Update endpoint return type to match what you are returning.
        // Here I'm assuming it is an array of Workout objects.
        getWorkouts: build.query<Workout[], void>({
          queryFn: async () => {
            try {
              const workouts = await firestore().collection('workouts').get();
              const data = workouts.docs.map((doc) => ({
                id: doc.id,
                name: doc.data().name,
                date: format(doc.data().date.toDate(), 'MM/dd/yyyy'),
              }));
    
              return { data };
            } catch (e) {
              return {
                error: {
                  status: 500,
                  message: (e as Error).message,
                },
              };
            }
          },
          onQueryStarted: async (arg, { queryFulfilled }) => {
            try {
              // data is Workout[] from queryFn
              const { data } = await queryFulfilled;
              workoutsAdapter.setAll(initialState, data);
            } catch(e) {
              // catch and handle/ignore error
            }
          },
        }),
      }),
    });
    

    Since you can't use queryFn and transformResponse together another suggestion might be to directly return the transformed entity data from the query function, i.e. something like return { data: workoutsAdapter.setAll(initialState, data) };.

    export const workoutApi = createApi({
      baseQuery: fakeBaseQuery<WorkoutError>(),
      endpoints: (build) => ({
        getWorkouts: build.query<Workout[], void>({
          queryFn: async () => {
            try {
              const workouts = await firestore().collection('workouts').get();
              const data = workouts.docs.map((doc) => ({
                id: doc.id,
                name: doc.data().name,
                date: format(doc.data().date.toDate(), 'MM/dd/yyyy'),
              }));
    
              return {
                data: workoutsAdapter.setAll(initialState, data)
              };
            } catch (e) {
              return {
                error: {
                  status: 500,
                  message: (e as Error).message,
                },
              };
            }
          },
        }),
      }),
    });