swr icon indicating copy to clipboard operation
swr copied to clipboard

Global mutate with a matcher function does not receive keys from useSWRInfinite

Open AayushBharti opened this issue 5 months ago • 2 comments

When using a matcher function with the global mutate API from useSWRConfig, keys generated by useSWRInfinite hooks are not passed to the matcher. This prevents the global invalidation of all pages of an infinite query, which is a necessary pattern for actions like adding a new item to a collection that affects all pages.

The same matcher function correctly receives keys from standard useSWR hooks, but it completely ignores the keys associated with useSWRInfinite (which are prefixed with $inf$).

Expected Behavior

I expect that a matcher function provided to the global mutate(matcher, ...) would be called with all active keys in the SWR cache. This should include the internal keys used by useSWRInfinite, allowing for global revalidation of infinite queries using a filter, like so:

mutate((key) => /boards/.test(key), null);

Observed Behavior

The matcher function is only ever called for keys associated with standard useSWR hooks. When logging the key argument inside the matcher, the keys managed by useSWRInfinite never appear, even though they are confirmed to be present in the cache.

As a result, attempting to globally mutate an infinite query with a regex or function matcher fails silently, as the relevant keys are never processed.

Steps to Reproduce

  1. Set up an infinite query: Create a component (Component A) that uses the useSWRInfinite hook to fetch paginated data.

    import useSWRInfinite from 'swr/infinite';
    
    const getKey = (pageIndex, previousPageData) => {
      if (previousPageData && !previousPageData.content.length) return null;
      return `/boards?page=${pageIndex}&perPage=25`;
    };
    
    const { data } = useSWRInfinite(getKey, fetcher);
    
  2. Attempt a global mutation: In a separate component (Component B) or global function, call mutate with a matcher function designed to target the infinite query keys.

    // Component B
    import { useSWRConfig } from 'swr';
    
    const { mutate } = useSWRConfig();
    
    const revalidateBoards = () => {
      mutate(
        (key) => {
          // This log will NEVER show keys starting with "$inf$/boards..."
          console.log('Key received by matcher:', key);
          return /^\$inf\$\/boards/.test(key);
        },
        null, // Revalidate
        { revalidate: true }
      );
    };
    
  3. Trigger the mutation: Call the revalidateBoards function.

  4. Observe the console: Notice that the console.log inside the matcher is never executed for the keys belonging to useSWRInfinite. The infinite query is not revalidated.

AayushBharti avatar Jul 27 '25 10:07 AayushBharti

When I traced back where this issue originated, it seems that it was first mentioned in Issue #1670 After that, in RFC #1946, there was a discussion that included plans to address this problem, and during the RFC discussion it was explicitly stated that special keys would be excluded.

Later, in PR #1989, this was actually implemented—special keys were excluded. However, the problem is that global mutate fails to update when special keys (specifically the $inf$ key) are ignored.

To address this, I attempted to add a new flag, includeSpecialKeys, to MutatorOptions. With this flag, I modified the condition at

// Skip the special useSWRInfinite and useSWRSubscription keys.

to include an exception. This initially seemed like it would work.

But after writing several edge test cases, I discovered an additional problem useSWRInfinite sets an _i flag in the cache whenever it calls its own mutate, and the fetcher relies on this flag to trigger a re-fetch. The problem is that global mutate has no way to include this _i flag. (Or rather, it’s something that can’t really be solved at the scope of a single PR from a regular contributor like myself.)

Of course, there could be potential solutions—for example, having the fetcher automatically set the _i flag during iteration. But that introduces ambiguity about responsibility. Another option would be to add a new flag, but that would require two new flags just for global mutate, which is also undesirable.

Therefore, my final approach was to extend the behavior so that when includeSpecialKeys is true, it also covers _i.

PR: https://github.com/vercel/swr/pull/4167

joseph0926 avatar Aug 25 '25 05:08 joseph0926

I had the same issue and after trying a number of things, i got the desired out come by doing this somewhat hackey workaround in my global mutator. Context: in my case, I want to match that a route starts with a given string (i.e. match all paths localhost:3000/parent/*

// target url is passed in via function call
const targetUrl: string = 'http://localhost:3000/parent-path'; // for example
return mutate<T>(
      (key) => {
        // Handle regular string keys
        if (typeof key === 'string') {
          const matches = key.startsWith(targetUrl);
          if (matches) {
            // additionally mutate any "infite" pages that match this key
            void mutate(`$inf$${key}`);
          }
          return matches
        }

        return false;
      },
      data,
      options,
    );

This results in the infinite page caches getting revalidated as expected.

neilpoulin avatar Sep 24 '25 18:09 neilpoulin