swr icon indicating copy to clipboard operation
swr copied to clipboard

TypeError undefined is not a function error on mutate

Open beetlebum opened this issue 2 years ago • 4 comments

After upgrading to swr 1.1 (currently using 1.1.1) we have observed the following behavior:

When calling mutate on a key that is no cached, a TypeError undefined is not a function error will be observed.

For Example:

const { mutate } = useSWRConfig()

      mutate(
            ITEMS_KEY,  
            (items: Item[]) =>
              items?.map((p) => (p.id === itemId ? update(p) : p)),
            false,
          )

if ITEMS_KEY was already loaded and cached, there will be no error. Also if I try to try/catch or do null checks inside the updater function, the error still happens. Worth noting we are using a custom cache implementation and this is with React Native.

I believe it somehow has to do with how that function is transformed into a generator function and the transpiled code might be doing some additional checks, because I can't console log it or debug it or try/catch it.

Any clues as to why this might be happening, or any suggestions as to how we might be using mutate incorrectly would be greatly appreciated.

beetlebum avatar Dec 12 '21 11:12 beetlebum

Hi @beetlebum ,

I tried to write a failing test for this case:

it.only('gracefully handles mutate on non existing keys', async () => {
    const fetcher = jest.fn(() => 'data')
    const mutSpy = jest.fn()

    const key = createKey()
    const mutKey = createKey()

    function Page() {
      const { mutate } = useSWRConfig()
      useSWR(key, fetcher)

      return (
        <div>
          <span>Hello</span>
          <button
            onClick={() =>
              mutate(
                mutKey,
                () => {
                  mutSpy()
                  return undefined
                },
                false
              )
            }
          >
            mutate
          </button>
        </div>
      )
    }

    renderWithConfig(<Page />)
    await screen.findByText('Hello')

    fireEvent.click(screen.getByRole('button'))

    expect(mutSpy).toHaveBeenCalledTimes(1)

    screen.getByText('Hello')
  })

But I observe no crashes. At least I am able to still see rendered content on screen.

In your snippet, where does update come from? Could you perhaps point how I could edit the test case above to reproduce your issue?

icyJoseph avatar Dec 13 '21 06:12 icyJoseph

Thanks for this @icyJoseph . I am quite confused by the whole thing so maybe let me give you more context:

  • This is in a React Native app, using the Hermes js engine.
  • SWRConfig includes a custom provider for the cache.
const mmkvProvider: () => ApiCache<any> = () => ({
  get: (key: string) => {
    const res = storage.getString(toKey(key));
    if (res) return JSON.parse(res);
    return res;
  },
  set: (key: string, value: any) => {
    if (value) {
      storage.set(toKey(key), JSON.stringify(value));
    } else {
      storage.set(toKey(key), '');
    }
  },
  keys: () => storage.getAllKeys(),
  clear: () => storage.clearAll(),
  delete: (key: string) => {
    storage.delete(toKey(key));
  },
  has: (key) => !!storage.getString(toKey(key)),
});

This provider has been working for a while now.

  • The crash is being reported thousands of times since releasing a version with SWR 1.1.0
  • Even though there's an exception, it does not crash the app (probably not running on the main thread?)

The code does something like this:

const { mutate } = useSWRConfig()
const editItem = async (itemId, update) => {
try {
  await Promise.all([
    mutate(`item_key_${itemId}`, item => item? update(item) : item),
    mutate(key_2, item => item.id === itemId? update(item) : item),
    mutate(key_3, items => items.map(i => i.id === itemId? update(i) : i)), // key_3 is not cached yet
  ])
} catch (error) {
  // This catch does not fire
}
}

beetlebum avatar Dec 13 '21 10:12 beetlebum

Is it possible to create a minimal reproduction? The crash shouldn't be related to React Native I think, but the custom cache provider seems suspicious.

shuding avatar Dec 23 '21 19:12 shuding

I have some more information about this issue, we're now using 1.3.0 but the issue still persists (React Native App).

We see many errors (does not crash the app) when trying to mutate a key that does not have a cached value, for example when trying to preload some content or update a local cache when the items was not loaded yet.

There's a stack trace now: Error: Cannot convert undefined value to object node_modules/swr/dist/index.js in __generator$argument_1 at line 298:66

                    _a = serialize(_key), key = _a[0], keyInfo = _a[2];
                    if (!key)
                        return [2 /*return*/];
                    _b = SWRGlobalState.get(cache), MUTATION = _b[2];

The last row is 298. image image

Please let me know if you need any more information.

beetlebum avatar Aug 08 '22 19:08 beetlebum

In case it helps: I ran into this issue myself when calling global mutate with a function in React Native, and removing the custom cache provider from my SWR config makes the issue go away. I narrowed it down to my get() function not handling non-string key types.

hramos avatar Mar 20 '23 21:03 hramos