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

Handling Dynamic Endpoint Arguments for Cache Updates

Open gusazevedo opened this issue 1 year ago • 2 comments

I'm attempting to update cache data from an endpoint, but I'm encountering a challenge with dynamic endpoint parameters. These parameters can vary due to pagination, making it difficult to pass them consistently.

I've tried to invalidate the cache using invalidatesTags and providesTags only. However, it seems that the backend does not process the result fast enough to update the frontend data upon refetch.

endpoints: (builder) => ({
  listAllUsers: builder.query({
      query: ({
          page = 0,
          page_size = 15,
          order = 'nickname',
          order_mode = 'asc',
          search = undefined,
      }) => ({
          url: '/admin/admins',
          method: 'GET',
          params: {page, page_size, order, order_mode, search},
      }),
  }),
  createUser: builder.mutation({
    query: (data) => ({
        url: '/admin/new',
        method: 'POST',
        body: {
            nickname: data.name,
            email: data.email,
            password: data.password,
            password_mode: 'permanent',
        },
    }),
    async onQueryStarted(props, {queryFulfilled, dispatch}) {
        const {data: newUser} = await queryFulfilled;
        const patchCollection = dispatch(
            apiSlice.util.updateQueryData('listAllUsers', {
                page: 0, // this prop should be dynamic
                page_size: 15, // this prop should be dynamic
            }, (draft) => {
                draft.users.splice(0, 0, {...newUser.admin, nickname: newUser.admin.name});
            }),
        );

        queryFulfilled.catch(patchCollection.undo);
    },
  }),
})

Explanation

Cache invalidation isn't working as expected because I'm required to pass the exact endpoint arguments from the 'listAllUsers' route to the updateQueryData function

Query on table component

const {data, isLoading} = useListAllUsersQuery({
        page,
        page_size: rowsPerPage,
        search: searchParam,
        // others...
    }, {
        refetchOnMountOrArgChange: true,
});

Goal

Be able to retrieve the args from some endpoint (listAllUsers) and pass to the updateQueryData function

const endpointArgs = someGetArgsFunction();

const patchCollection = dispatch(
  apiSlice.util.updateQueryData('listAllUsers', {
      endpointArgs
  }, (draft) => {
      draft.users.splice(0, 0, {...newUser.admin, nickname: newUser.admin.name});
  }),
);

gusazevedo avatar Jan 10 '24 06:01 gusazevedo

I'm currently having a similar problem, anyone with a solution or a workaround for this?

rafaelgmota avatar Jan 12 '24 12:01 rafaelgmota

@gusazevedo Hi, with the code you provided and the concept you describe a point is missing. Let me explain

When you build this query

  listAllUsers: builder.query({
      query: ({
          page = 0,
          page_size = 15,
          order = 'nickname',
          order_mode = 'asc',
          search = undefined,
      }) => ({
          url: '/admin/admins',
          method: 'GET',
          params: {page, page_size, order, order_mode, search},
      }),
  }),

a single cache entry is created each time a param changes. The cacheKey for that entry will be the endpointName + the combination of those params. So for example when you change the page param a new cache entry is going to be created. Now on creating a new user, you actually need to tell the updateQueryData function in which cache entry for that endpointName, you want to add the newly created user. A simple approach would be to provide a single tag to that listAllUsers query and then retrieve the cache entries for that tag using selectInvalidatedBy function. Like so:

  listAllUsers: builder.query({
      query: ({
          page = 0,
          page_size = 15,
          order = 'nickname',
          order_mode = 'asc',
          search = undefined,
      }) => ({
          url: '/admin/admins',
          method: 'GET',
          params: {page, page_size, order, order_mode, search},
      }),
     providesTags: ["Users"]
  }),

then in createUser mutation

  async onQueryStarted(props, {queryFulfilled, dispatch}) {
        const {data: newUser} = await queryFulfilled;

       const entries = apiSlice.util.selectInvalidatedBy(store.getState(), ["Users"]);

       //each entry will be an object containing endpointName , originalArgs and queryCacheKey
      //so right here you decide in which entry you want to add the new user. Lets assume we add him to the first entry

       const {originalArgs} = entries[0];

        const patchCollection = dispatch(
            apiSlice.util.updateQueryData('listAllUsers', originalArgs, (draft) => {
                draft.users.splice(0, 0, {...newUser.admin, nickname: newUser.admin.name});
            }),
        );

        queryFulfilled.catch(patchCollection.undo);
    },

You can also create and give more complex tags to the listAllUsers query, in order to have better control on the cache entries you want to retrieve using selectInvalidatedBy.

kkatsi avatar Feb 18 '24 10:02 kkatsi