Skip to main content

cancelQueriesForKeys

A batch helper function that cancels in-flight queries for multiple TanStack Query cache keys. This is essential for preventing race conditions during optimistic updates.

Type Signature​

export const cancelQueriesForKeys = (
keys: Array<QueryKey<string>>,
): void

Parameters​

keys​

  • Type: Array<QueryKey<string>>
  • Required: Yes
  • Description: Array of QueryKey objects for which to cancel in-flight queries

Return Value​

  • Type: void
  • Description: This function performs side effects (cancellation) and returns nothing

How It Works​

The function iterates through the provided array of QueryKeys and calls queryClient.cancelQueries() for each one. It automatically filters out any null or undefined values.

export const cancelQueriesForKeys = (
keys: Array<QueryKey<string>>,
): void => {
keys.filter(Boolean).forEach((key) => {
queryClient.cancelQueries({ queryKey: [key] });
});
};

Why Cancel Queries?​

When performing optimistic updates, you must cancel any in-flight queries that might overwrite your optimistic data. This prevents race conditions where:

  1. User triggers mutation
  2. Mutation starts
  3. Background refetch completes and overwrites optimistic update
  4. Mutation completes but user sees stale data briefly

Basic Example​

import { cancelQueriesForKeys } from 'src/queries';
import { accountsQueryGroup } from './queryGroups';

// Cancel in-flight account queries before optimistic update
await cancelQueriesForKeys([
accountsQueryGroup.list.queryKey,
accountsQueryGroup.detail.queryKey('account-123'),
]);

Common Use Cases​

1. Optimistic Updates (Most Important)​

The primary use case is preventing race conditions during optimistic updates:

export const useTransactionCreate = ({ onSuccess, ...rest }) =>
generatedTransactionCreate({
mutation: {
mutationKey: [transactionsQueryGroup.create.queryKey],

// Cancel queries BEFORE making optimistic changes
onMutate: async (variables) => {
// Cancel all queries that we're about to update
await cancelQueriesForKeys([
transactionsQueryGroup.list.queryKey,
accountsQueryGroup.detail.queryKey(variables.data.accountId),
]);

// Snapshot previous state for rollback
const previousTransactions = queryClient.getQueryData([
transactionsQueryGroup.list.queryKey,
]);

// Perform optimistic update
queryClient.setQueryData(
[transactionsQueryGroup.list.queryKey],
(old: any) => [...(old || []), variables.data]
);

return { previousTransactions };
},

// Rollback on error
onError: (error, variables, context) => {
if (context?.previousTransactions) {
queryClient.setQueryData(
[transactionsQueryGroup.list.queryKey],
context.previousTransactions
);
}
},

// Refetch on success
onSuccess: (data, variables, context) => {
invalidateQueriesForKeys([
transactionsQueryGroup.list.queryKey,
accountsQueryGroup.detail.queryKey(data.accountId),
]);
},

...rest,
},
});

2. Multi-Entity Optimistic Updates​

When a mutation affects multiple entities:

export const useAccountUpdate = ({ onSuccess, ...rest }) =>
generatedAccountUpdate({
mutation: {
onMutate: async (variables) => {
// Cancel all affected queries
await cancelQueriesForKeys([
accountsQueryGroup.list.queryKey,
accountsQueryGroup.detail.queryKey(variables.id),
balancesQueryGroup.list.queryKey,
dashboardQueryGroup.summary.queryKey,
]);

// Snapshot all affected caches
const previousAccount = queryClient.getQueryData([
accountsQueryGroup.detail.queryKey(variables.id),
]);
const previousList = queryClient.getQueryData([
accountsQueryGroup.list.queryKey,
]);

// Optimistic updates...
queryClient.setQueryData(
[accountsQueryGroup.detail.queryKey(variables.id)],
{ ...previousAccount, ...variables.data }
);

return { previousAccount, previousList };
},

onError: (error, variables, context) => {
// Rollback all caches
if (context?.previousAccount) {
queryClient.setQueryData(
[accountsQueryGroup.detail.queryKey(variables.id)],
context.previousAccount
);
}
if (context?.previousList) {
queryClient.setQueryData(
[accountsQueryGroup.list.queryKey],
context.previousList
);
}
},

...rest,
},
});

3. Canceling During Navigation​

Cancel in-flight queries when user navigates away:

const handleNavigateAway = async () => {
// Cancel any pending queries before unmounting
await cancelQueriesForKeys([
accountsQueryGroup.list.queryKey,
transactionsQueryGroup.list.queryKey,
]);

navigate('/dashboard');
};

The Optimistic Update Pattern​

Here's the complete pattern with cancellation:

export const useItemUpdate = ({ onSuccess, ...rest }) =>
generatedItemUpdate({
mutation: {
// 1. CANCEL - Prevent race conditions
onMutate: async (variables) => {
await cancelQueriesForKeys([
itemsQueryGroup.list.queryKey,
itemsQueryGroup.detail.queryKey(variables.id),
]);

// 2. SNAPSHOT - Save previous state
const previousItems = queryClient.getQueryData([
itemsQueryGroup.list.queryKey,
]);

// 3. OPTIMISTIC UPDATE - Update UI immediately
queryClient.setQueryData(
[itemsQueryGroup.list.queryKey],
(old: any) =>
old.map((item: any) =>
item.id === variables.id ? { ...item, ...variables.data } : item
)
);

return { previousItems };
},

// 4. ROLLBACK - Restore on error
onError: (error, variables, context) => {
if (context?.previousItems) {
queryClient.setQueryData(
[itemsQueryGroup.list.queryKey],
context.previousItems
);
}
},

// 5. INVALIDATE - Fetch fresh data on success
onSuccess: (data, variables, context) => {
invalidateQueriesForKeys([
itemsQueryGroup.list.queryKey,
itemsQueryGroup.detail.queryKey(data.id),
]);
},

...rest,
},
});

Advanced Patterns​

Conditional Cancellation​

Only cancel queries that might be affected:

onMutate: async (variables) => {
const keysToCancel = [itemsQueryGroup.list.queryKey];

// Only cancel detail query if we're updating a specific item
if (variables.id) {
keysToCancel.push(itemsQueryGroup.detail.queryKey(variables.id));
}

// Only cancel related queries if relationship changed
if (variables.data.categoryId) {
keysToCancel.push(categoriesQueryGroup.list.queryKey);
}

await cancelQueriesForKeys(keysToCancel);

// ...optimistic updates
};

With TypeScript Context​

Properly type your context for rollback:

interface MutationContext {
previousList: Transaction[] | undefined;
previousDetail: Transaction | undefined;
affectedAccountId: string;
}

export const useTransactionUpdate = ({ onSuccess, ...rest }) =>
generatedTransactionUpdate({
mutation: {
onMutate: async (variables): Promise<MutationContext> => {
await cancelQueriesForKeys([
transactionsQueryGroup.list.queryKey,
transactionsQueryGroup.detail.queryKey(variables.id),
]);

const previousList = queryClient.getQueryData<Transaction[]>([
transactionsQueryGroup.list.queryKey,
]);

const previousDetail = queryClient.getQueryData<Transaction>([
transactionsQueryGroup.detail.queryKey(variables.id),
]);

// Optimistic updates...

return {
previousList,
previousDetail,
affectedAccountId: variables.data.accountId,
};
},

onError: (error, variables, context?: MutationContext) => {
if (context?.previousList) {
queryClient.setQueryData(
[transactionsQueryGroup.list.queryKey],
context.previousList
);
}
if (context?.previousDetail) {
queryClient.setQueryData(
[transactionsQueryGroup.detail.queryKey(variables.id)],
context.previousDetail
);
}
},

...rest,
},
});

Comparison: With vs. Without Cancellation​

Without Cancellation (Race Condition Risk)​

// ❌ DANGEROUS - Race condition possible
onMutate: async (variables) => {
// No cancellation!

const previous = queryClient.getQueryData([itemsQueryGroup.list.queryKey]);

queryClient.setQueryData([itemsQueryGroup.list.queryKey], (old) => [
...(old || []),
variables.data,
]);

return { previous };

// If a background refetch completes here, it will overwrite our optimistic update!
};

With Cancellation (Safe)​

// ✅ SAFE - Race condition prevented
onMutate: async (variables) => {
// Cancel any in-flight queries first
await cancelQueriesForKeys([itemsQueryGroup.list.queryKey]);

const previous = queryClient.getQueryData([itemsQueryGroup.list.queryKey]);

queryClient.setQueryData([itemsQueryGroup.list.queryKey], (old) => [
...(old || []),
variables.data,
]);

return { previous };

// No background refetch can overwrite our changes
};

When NOT to Use​

Simple Mutations Without Optimistic Updates​

If you're just invalidating after success, cancellation isn't necessary:

// No optimistic update = no need for cancellation
export const useSimpleUpdate = ({ onSuccess, ...rest }) =>
generatedSimpleUpdate({
mutation: {
mutationKey: [itemsQueryGroup.update.queryKey],

// Just invalidate on success
onSuccess: (data, variables, context) => {
invalidateQueriesForKeys([
itemsQueryGroup.list.queryKey,
itemsQueryGroup.detail.queryKey(data.id),
]);
},

...rest,
},
});

Performance Considerations​

Cancel Only What's Necessary​

// GOOD - Cancel only affected queries
await cancelQueriesForKeys([
accountsQueryGroup.detail.queryKey(accountId),
]);

// AVOID - Don't cancel unrelated queries
await cancelQueriesForKeys([
accountsQueryGroup.all.queryKey, // Too broad!
]);

Async/Await is Required​

Always await the cancellation:

// GOOD
onMutate: async (variables) => {
await cancelQueriesForKeys([...]);
// Now safe to update
};

// BAD - Race condition still possible
onMutate: async (variables) => {
cancelQueriesForKeys([...]); // Not awaited!
// Updates might happen before cancellation completes
};

Best Practices​

1. Always Cancel Before Optimistic Updates​

onMutate: async (variables) => {
// 1. Cancel first
await cancelQueriesForKeys([...]);

// 2. Then update
queryClient.setQueryData([...], ...);
};

2. Cancel All Affected Queries​

Think about what queries your mutation affects:

// Account update affects multiple entities
await cancelQueriesForKeys([
accountsQueryGroup.detail.queryKey(id),
accountsQueryGroup.list.queryKey,
balancesQueryGroup.list.queryKey, // Balance depends on account
dashboardQueryGroup.summary.queryKey, // Summary includes account data
]);

3. Use with invalidateQueriesForKeys​

Combine both helpers for complete optimistic update flow:

import { cancelQueriesForKeys, invalidateQueriesForKeys } from 'src/queries';

// onMutate: cancel
await cancelQueriesForKeys([...]);

// onSuccess: invalidate
invalidateQueriesForKeys([...]);

See Also​