react-query-kit icon indicating copy to clipboard operation
react-query-kit copied to clipboard

What is the best practice for invalidating queries in react-query-kit

Open inceenes10 opened this issue 1 year ago • 4 comments

I generally use the approach in the code below.

import { queryClient } from "@sustable/system";

export const useDeleteUserMutation = createMutation({
	mutationKey: ["corporate", "user", "delete"],
	mutationFn: async (data: IDeleteUserRequest) => {
		await agent.delete("/user/delete", { data });
	},
	onSuccess: () =>
		queryClient.invalidateQueries({
			queryKey: ["corporate", "user", "list"],
		}),
});

This approach does not use useQueryClient() hook and I just wanna know if this approach has any side effect. If there is a side effect, what can I do to prevent that side effect? Is middleware like approach a better solution for this? If it is, could we add this to documentation of react-query-kit

Thank in advance.

inceenes10 avatar Jul 09 '24 18:07 inceenes10

Yeah, you can customize any behavior of this hook via middleware

liaoliao666 avatar Jul 10 '24 01:07 liaoliao666

Here's the middleware I created

const invalidateQueriesAfterMutation = (keys) =>
  (useMutationNext) => {
    return (options) => {
      const queryClient = useQueryClient()

      const onSuccess = (...params) => {
        keys.forEach((key) => {
          if ('guard' in key) {
            if (key.guard(...params)) {
              queryClient.invalidateQueries(key.queryKey)
            }
          } else {
            queryClient.invalidateQueries(key)
          }
        })
        options.onSuccess?.(...params)
      }

      return useMutationNext({ ...options, onSuccess })
    }
  }

Then it can be used:

use: [invalidateQueriesAfterMutation([useFooHook.getKey(), useBarHook.getKey()])],

If you need to invalidate the query conditionally, based on the respond, then use the guard function. If the guard function returns true, the query with the given key is invalidated.

  use: [
    invalidateQueriesAfterMutation([
      {
        guard: (data) => data.foobar === 'baz'
        queryKey: useBazHook.getKey(),
      },
    ])

If your queries are created with RQK, it provides getKey() method to retrieve the query key instead of using the hardcoded string.

denisborovikov avatar Aug 26 '24 16:08 denisborovikov

@denisborovikov Hi, thanks for the Idea!

I extended it to support Typescript and having possibility to specify method in which we have to invalidate, as well as possibility to provide invalidateOptions and waitForInvalidation.


import { Middleware, MutationHook } from "react-query-kit";
import {
  InvalidateOptions,
  InvalidateQueryFilters,
  QueryKey,
  useQueryClient,
} from "@tanstack/react-query";

import { HandledCustomError } from "~shared/api/lib/handleApiError";

type GuardFunctionOnSuccess<TData, TVariables, TContext> = (
  data: TData,
  variables: TVariables,
  context: TContext
) => boolean;

type GuardFunctionOnSettled<
  TData,
  TError extends HandledCustomError,
  TVariables,
  TContext
> = (
  data: TData | undefined,
  error: TError | null,
  variables: TVariables,
  context: TContext | undefined
) => boolean;

interface FiltersInvalidateEntry {
  type: "filters";
  queryFilters: InvalidateQueryFilters;
}

interface GuardInvalidateEntryOnSuccess<TData, TVariables, TContext> {
  type: "guard";
  queryFilters: InvalidateQueryFilters;
  guard: GuardFunctionOnSuccess<TData, TVariables, TContext>;
}

interface GuardInvalidateEntryOnSettled<
  TData,
  TError extends HandledCustomError,
  TVariables,
  TContext
> {
  type: "guard";
  queryFilters: InvalidateQueryFilters;
  guard: GuardFunctionOnSettled<TData, TError, TVariables, TContext>;
}

type InvalidateEntryOnSuccess<TData, TVariables, TContext> =
  | FiltersInvalidateEntry
  | GuardInvalidateEntryOnSuccess<TData, TVariables, TContext>;

type InvalidateEntryOnSettled<
  TData,
  TError extends HandledCustomError,
  TVariables,
  TContext
> =
  | FiltersInvalidateEntry
  | GuardInvalidateEntryOnSettled<TData, TError, TVariables, TContext>;

type InvalidateEntriesOnSuccess<TData, TVariables, TContext> = (
  | QueryKey
  | InvalidateEntryOnSuccess<TData, TVariables, TContext>
)[];

type InvalidateEntriesOnSettled<
  TData,
  TError extends HandledCustomError,
  TVariables,
  TContext
> = (
  | QueryKey
  | InvalidateEntryOnSettled<TData, TError, TVariables, TContext>
)[];

function invalidateQueriesAfterMutationMiddleware<
  TData,
  TVariables,
  TError extends HandledCustomError,
  TContext
>(
  entries: InvalidateEntriesOnSuccess<TData, TVariables, TContext>,
  options: {
    method?: "onSuccess";
    invalidateOptions?: InvalidateOptions;
    waitForInvalidation?: boolean;
  }
): Middleware<MutationHook<TData, TVariables, TError, TContext>>;

function invalidateQueriesAfterMutationMiddleware<
  TData,
  TVariables,
  TError extends HandledCustomError,
  TContext
>(
  entries: InvalidateEntriesOnSettled<TData, TError, TVariables, TContext>,
  options?: {
    method?: "onSettled";
    invalidateOptions?: InvalidateOptions;
    waitForInvalidation?: boolean;
  }
): Middleware<MutationHook<TData, TVariables, TError, TContext>>;

function invalidateQueriesAfterMutationMiddleware<
  TData,
  TVariables,
  TError extends HandledCustomError,
  TContext
>(
  entries:
    | InvalidateEntriesOnSuccess<TData, TVariables, TContext>
    | InvalidateEntriesOnSettled<TData, TError, TVariables, TContext>,
  options?: {
    method?: "onSuccess" | "onSettled";
    invalidateOptions?: InvalidateOptions;
    waitForInvalidation?: boolean;
  }
): Middleware<MutationHook<TData, TVariables, TError, TContext>> {
  return (useMutationNext) => {
    return (mutationOptions) => {
      const queryClient = useQueryClient();

      const method = options?.method ?? "onSettled";
      const shouldWaitForInvalidation = options?.waitForInvalidation ?? false;

      if (method === "onSuccess") {
        const onSuccess = async (
          data: TData,
          variables: TVariables,
          context: TContext
        ) => {
          const invalidationPromises: Promise<void>[] = [];

          for (const entry of entries as InvalidateEntriesOnSuccess<
            TData,
            TVariables,
            TContext
          >) {
            if (!Array.isArray(entry)) {
              const keyEntry = entry as InvalidateEntryOnSuccess<
                TData,
                TVariables,
                TContext
              >;

              // Entry is InvalidateEntryOnSuccess
              if (keyEntry.type === "filters") {
                const promise = queryClient.invalidateQueries(
                  keyEntry.queryFilters,
                  options?.invalidateOptions
                );

                invalidationPromises.push(promise);
              } else if (keyEntry.type === "guard") {
                if (keyEntry.guard(data, variables, context)) {
                  const promise = queryClient.invalidateQueries(
                    keyEntry.queryFilters,
                    options?.invalidateOptions
                  );

                  invalidationPromises.push(promise);
                }
              }
            } else {
              // Entry is QueryKey
              const promise = queryClient.invalidateQueries(
                { queryKey: entry },
                options?.invalidateOptions
              );

              invalidationPromises.push(promise);
            }
          }

          if (shouldWaitForInvalidation) {
            await Promise.all(invalidationPromises);
          }

          const originalOnSuccess = mutationOptions.onSuccess;
          const result = originalOnSuccess?.(data, variables, context);

          if (shouldWaitForInvalidation && result instanceof Promise) {
            await result;
          }
        };

        return useMutationNext({ ...mutationOptions, onSuccess });
      } else if (method === "onSettled") {
        const onSettled = async (
          data: TData | undefined,
          error: TError | null,
          variables: TVariables,
          context: TContext | undefined
        ) => {
          const invalidationPromises: Promise<void>[] = [];

          for (const entry of entries as InvalidateEntriesOnSettled<
            TData,
            TError,
            TVariables,
            TContext
          >) {
            if (typeof entry !== "string" && !Array.isArray(entry)) {
              const keyEntry = entry as InvalidateEntryOnSettled<
                TData,
                TError,
                TVariables,
                TContext
              >;

              // Entry is InvalidateEntryOnSettled
              if (keyEntry.type === "filters") {
                const promise = queryClient.invalidateQueries(
                  keyEntry.queryFilters,
                  options?.invalidateOptions
                );

                invalidationPromises.push(promise);
              } else if (keyEntry.type === "guard") {
                if (keyEntry.guard(data, error, variables, context)) {
                  const promise = queryClient.invalidateQueries(
                    keyEntry.queryFilters,
                    options?.invalidateOptions
                  );

                  invalidationPromises.push(promise);
                }
              }
            } else {
              // Entry is QueryKey
              const promise = queryClient.invalidateQueries(
                { queryKey: entry },
                options?.invalidateOptions
              );

              invalidationPromises.push(promise);
            }
          }

          if (shouldWaitForInvalidation) {
            await Promise.all(invalidationPromises);
          }

          const originalOnSettled = mutationOptions.onSettled;
          const result = originalOnSettled?.(data, error, variables, context);

          if (shouldWaitForInvalidation && result instanceof Promise) {
            await result;
          }
        };

        return useMutationNext({ ...mutationOptions, onSettled });
      }

      return useMutationNext(mutationOptions);
    };
  };
}

export default invalidateQueriesAfterMutationMiddleware;

vlanemcev avatar Sep 19 '24 16:09 vlanemcev

Here's the example of usage:


export const useAnotherMutation = createMutation({
  mutationFn: async (variables) => {
    // Your mutation logic
  },
  use: [
    invalidateQueriesAfterMutationMiddleware(
      [
        someQuery.getKey(), 
        ['some query key']
        {
          type: 'guard',
          guard: (data, error, variables, context) => {
            // Correct parameters for onSettled

            return !error && data?.needsRefresh;
          },
          queryFilters: { queryKey: ['inventory'] },
        },
      ],
      {
        method: 'onSettled', // by default
        waitForInvalidation: false, // by default
      }
    ),
  ],
});

vlanemcev avatar Sep 19 '24 16:09 vlanemcev