Skip to content

Custom compare function in useSWR is called excessively #4153

@oliver-lindemann

Description

@oliver-lindemann

Bug report

Description / Observed Behavior

When using a custom compare function in the useSWR hook, the function is called repeatedly (depending on how many React components are using the hook), rather than only once after new data has been fetched. This leads to a performance bottleneck, especially when the custom comparison logic is complex, such as a deep comparison of large data arrays. The console.log statement within the compare function confirms this behavior, showing it being triggered an excessive number of times.

Expected Behavior

The compare function should be invoked only once per revalidation cycle, specifically after the fetcher has successfully returned new data. Its purpose is to determine whether the newly fetched data is different from the data currently in the cache, and thereby prevent unnecessary state updates and re-renders if the data is functionally the same.

Repro Steps / Code Example

const useUsers = () => {
  const fetcher: Fetcher<IUser[], string> = getUsers;

  const { data: users, ...rest } = useSWR("/users", fetcher, {
    // Compare new fetched data with existing cached data
    // Determine if the data has changed
    compare(cachedData, newData) {
      // compare should only be called once if new data was fetched
      // but get's called countless times (depending on how many React components are using useUsers() hook)
      console.log({ cachedData, newData });

      // Custom compare logic

      // shallow compare
      if (!cachedData || !newData) return cachedData === newData;
      if (cachedData.length !== newData.length) return false;

      // Deep compare
      const mapOld = new Map(cachedData.map((user) => [user._id, user]));
      const mapNew = new Map(newData.map((user) => [user._id, user]));

      for (const [_id, oldUser] of mapOld) {
        const newUser = mapNew.get(_id);
        if (!newUser || !areUsersEqual(oldUser, newUser)) return false;
      }

      return true;
    },
  });

  return {
    users,
    ...rest,
  };
};

export default useUsers;

Additional Context

SWR version: 2.3.4

After I looked into the SWR source code in use-swr.ts, I found a section that appears to be the intended behavior:

finalState.data = compare(cacheData, newData) ? cacheData : newData

https://github.com/vercel/swr/blob/main/src/index/use-swr.ts#L483

but I do not fully understand the meaning of the following code and I am not sure if this might trigger the excessive amount of compare function calls.

  const isEqual = (prev: State<Data, any>, current: State<Data, any>) => {
    for (const _ in stateDependencies) {
      const t = _ as keyof StateDependencies
      if (t === 'data') {
        if (!compare(prev[t], current[t])) {
          if (!isUndefined(prev[t])) {
            return false
          }
          if (!compare(returnedData, current[t])) {
            return false
          }
        }
      } else {
        if (current[t] !== prev[t]) {
          return false
        }
      }
    }
    return true
  }

https://github.com/vercel/swr/blob/main/src/index/use-swr.ts#L154C1-L173C4

Am I missing something here or is this a faulty behavior?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions