What is the best practice for invalidating queries in react-query-kit
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.
Yeah, you can customize any behavior of this hook via middleware
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 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;
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
}
),
],
});