swr: Multiple Arguments with object in render breaks cache

Hi,

It seems that multiple arguments with object in render breaks cache :

// Make sure objects are stable
const params = useMemo(() => ({ id }), [id])
useSWR(['/api/user', params], query)

If you follow this use case : id = 1 --> id = 2 --> id = 1 The second id = 1 doesn’t have the same key in cache as the first (because params doesn’t have the same ref)

Reproductible codesandbox : https://codesandbox.io/s/swr-multiple-arguments-no-cache-mfssd

The only way I found to preserve cache is to stringify in memo and parse in fetcher (https://codesandbox.io/s/swr-multiple-arguments-cache-p8iwy)

const params = useMemo(() => JSON.stringify({ id }), [id])

any better idea ?

Thanks !

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 3
  • Comments: 16 (5 by maintainers)

Most upvoted comments

🎉 Added support for key serialization in #1429 and released as 1.1.0-beta.0, you don’t need useMemo anymore. CC: @nandorojo

Yes this is indeed a problem. In our real world use cases, we’re using it like this:

const { data: user } = useSWR('/api/user', fetch)
const { data: userItems } = useSWR(['/api/items', user], fetchWithUser)

Because user is a globally shared object, the cache key of the second query will always be stable. For use cases like query params, I will still suggest passing primitive values as args directly (will update documentation as well):

useSWR(['/api/user', id], (url, id) => query(url, { id }))

We have some discussions here https://github.com/zeit/swr/pull/145#discussion_r350064725.

We now have an example on how to add key-serialization with SWR v1 Middleware: Serialize Object Keys. This can solve the original issue in this thread, maybe someone can publish it as a handy library?

Deep comparison would be a great addition, but I’m still having concerns about the performance (as well as serialization). Imaging a huge list with useSWR hooks inside, each re-render needs to run a synchronized deep comparison. We need to get some benchmarking for this.

My current working solution :

import useSWR from 'swr';
import { useMemo } from 'react';

export default (key) => {
  const k = useMemo(() => {
    return key;
  }, [JSON.stringify(key)]);
  return useSWR(k);
};

@nandorojo Yeah we could provide an option to support custom key serialization. There’re probably some more efficient JSON stringify implementations, but I’m not sure about type safety…

react-query provides deep-equal support for items in an array key. Is this the type of thing we could implement? Maybe a custom didKeyChange function could live in the config?

<SWRConfig value={{ didKeyChange: (prevKey, nextKey) => deepEqual(prevKey, nextKey) }} />

I know that deep-equal checks aren’t ideal, and it is slow to run on every render. However, for use-cases with large objects as a key, a plain array key has bad type safety. For instance, I have an algolia search wrapper with swr.

The SWR key takes all of these fields, so I currently use JSON.stringify

const key = [
  'algolia-search',
  query,
  index,
  JSON.stringify({
    facets,
    filters,
    facetFilters,
    numericFilters,
    hitsPerPage,
    page,
    aroundLatLng,
    aroundLatLngViaIP,
    aroundRadius,
    cacheable
  })
]

useSWR(key, algoliaSearch)

Passing each options as primitives to an array isn’t great. However, stringifying it also isn’t very good, since I have to then JSON.parse inside of the fetcher and cast the types to it:

const fetcher = async (_, query = '', index, facetString) => {
      const {
        facets = [],
        numericFilters,
        facetFilters,
        filters,
        hitsPerPage,
        page,
        aroundLatLngViaIP,
        aroundLatLng,
        aroundRadius,
        cacheable,
      }: Pick<
        SWRKeyParameters<T>,
        | 'facets'
        | 'filters'
        | 'facetFilters'
        | 'numericFilters'
        | 'hitsPerPage'
        | 'page'
        | 'aroundLatLng'
        | 'aroundLatLngViaIP'
        | 'aroundRadius'
        | 'cacheable'
      > = facetString ? JSON.parse(facetString) ?? {} : undefined
}

Is there a type-safe solution that is more efficient than JSON.stringify?

@shuding I’m also coming across this problem. Is there any newer recommended solution for this? Our use case is essentially making a generic fetching function which needs to be able to accept flexible variables, so passing primitive values as args directly doesn’t fit the use-case.

Maybe useSWR should deep-compare the input, just like it deep compares output? (possibly could be a config option?) Thinking that this would solve the issue you mentioned in your comment on the axios example linked above.