Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
inceenes10 opened this issue Jul 9, 2024 · 4 comments
Open

Comments

@inceenes10
Copy link

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.

@liaoliao666
Copy link
Collaborator

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

@denisborovikov
Copy link

denisborovikov commented Aug 26, 2024

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.

@vlanemcev
Copy link

@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
Copy link

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
      }
    ),
  ],
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants