vercel / swr

React Hooks for Data Fetching
https://swr.vercel.app
MIT License
30.24k stars 1.21k forks source link

Calling `setSize` before pages finish loading causes `useSWRInfinite` to trigger multiple requests #3001

Open FelixZY opened 1 month ago

FelixZY commented 1 month ago

Bug report

Description / Observed Behavior

In an infinite scroll scenario, useSWRInfinite would trigger tens of duplicated requests in the span of a few milliseconds.

Expected Behavior

SWR should have triggered only one request

Repro Steps / Code Example

Minimal reproduction set up here: https://github.com/FelixZY/swr-setsize-bug-repro

Using the linked reproduction:

  1. start the development server using npm start
  2. open the console drawer of your browser's development tools (I'm using google chrome)
  3. hover your cursor over the website
  4. scroll the mouse wheel

Expected:

The console output should show

/0: 1
/1: 1
/2: 1
/3: 1
/4: 1
/5: 1
/6: 1
/7: 1
/8: 1
/9: 1

Actual:

Actual output (example) ``` /0: 1 /1: 1 /1: 2 /1: 3 /1: 4 /1: 5 /1: 6 /1: 7 /1: 8 /1: 9 /1: 10 /1: 11 /2: 1 /2: 2 /2: 3 /2: 4 /2: 5 /2: 6 /2: 7 /2: 8 /2: 9 /2: 10 /2: 11 /2: 12 /2: 13 /2: 14 /2: 15 /2: 16 /3: 1 /3: 2 /3: 3 /3: 4 /3: 5 /3: 6 /3: 7 /3: 8 /3: 9 /3: 10 /3: 11 /3: 12 /3: 13 /3: 14 /3: 15 /3: 16 /3: 17 /3: 18 /3: 19 /4: 1 /4: 2 /4: 3 /4: 4 /4: 5 /4: 6 /4: 7 /4: 8 /4: 9 /4: 10 /4: 11 /4: 12 /4: 13 /4: 14 /4: 15 /4: 16 /4: 17 /4: 18 /4: 19 /4: 20 /5: 1 /5: 2 /5: 3 /5: 4 /5: 5 /5: 6 /5: 7 /5: 8 /5: 9 /5: 10 /5: 11 /5: 12 /5: 13 /5: 14 /5: 15 /5: 16 /5: 17 /5: 18 /5: 19 /5: 20 /6: 1 /6: 2 /6: 3 /6: 4 /6: 5 /6: 6 /6: 7 /6: 8 /6: 9 /6: 10 /6: 11 /6: 12 /6: 13 /6: 14 /6: 15 /6: 16 /6: 17 /6: 18 /6: 19 /6: 20 /7: 1 /7: 2 /7: 3 /7: 4 /7: 5 /7: 6 /7: 7 /7: 8 /7: 9 /7: 10 /7: 11 /7: 12 /7: 13 /7: 14 /7: 15 /7: 16 /7: 17 /7: 18 /7: 19 /7: 20 /8: 1 /8: 2 /8: 3 /8: 4 /8: 5 /8: 6 /8: 7 /8: 8 /8: 9 /8: 10 /8: 11 /8: 12 /8: 13 /8: 14 /8: 15 /8: 16 /8: 17 /8: 18 /8: 19 /8: 20 /9: 1 /9: 2 /9: 3 /9: 4 /9: 5 /9: 6 /9: 7 /9: 8 /9: 9 /9: 10 /9: 11 /9: 12 /9: 13 /9: 14 /9: 15 /9: 16 /9: 17 /9: 18 /9: 19 /9: 20 ```

Screencast from 2024-08-08 13:31:48.webm

Additional Context

SWR version: 2.2.5

It seems the crucial step is for setSize to be called multiple times before loading has actually finished. You can confirm this by removing delay here:

https://github.com/FelixZY/swr-setsize-bug-repro/blob/22ae6eb8181b227739fd0512444501a3a14bb3d6/src/App.js#L20

FelixZY commented 1 month ago

For now, it is possible to work around the issue using this wrapper function:

import { useCallback } from "react";
import useSWRInfinite, { SWRInfiniteHook } from "swr/dist/infinite";

export const useSWRInfinitePatched: SWRInfiniteHook =
  function useSWRInfinitePatched(this: unknown, ...params: unknown[]) {
    const hook = useSWRInfinite.apply(this, params as any);

    const originalSetSize = hook.setSize;
    hook.setSize = useCallback(
      (sizeOrFn) => {
        const newSize =
          typeof sizeOrFn === "function" ? sizeOrFn(hook.size) : sizeOrFn;

        // Patch for
        // https://github.com/vercel/swr/issues/3001
        if (newSize === hook.size) {
          // Based on
          // https://github.com/vercel/swr/blob/1585a3e37d90ad0df8097b099db38f1afb43c95d/src/infinite/index.ts#L295
          return Promise.resolve() as Promise<undefined>;
        } else {
          return originalSetSize(newSize);
        }
      },
      [originalSetSize, hook.size]
    );

    return hook;
  } as unknown as SWRInfiniteHook;
jlow2397 commented 1 month ago

hello! i'm working on an email app with infinite loading and was struggling to figure out why our api was getting pinged multiple times on fast scroll scenarios. in our case, we have optimistic ui that was loading and increasing the size of the infinite loader, but the api wasn't finished with a subsequent api call.

the crux of the issue is that the useSWRInfinite is playing "catch up" with our optimistic ui. for example, the optimisic ui would set the size to 10, but the api would be still loading the 2nd page of data.

I believe that this probably goes against the use case of useSWRInfinite, where one page should theoretically be loaded before another page can even start loading, and I think this is apparent in the source code:

https://github.com/vercel/swr/blob/main/src/infinite/index.ts#L184

in the "catch up scenario", the 10th call of the fetcher function notices that the pages 2-10 need loading, so it will trigger an api request for pages 2-10, but the 9th call of the fetcher function notices that pages 2-9 need loading so it will trigger an additional api request for pages 2-9, etc.


I figured that this probably won't be fixed with SWR (since it goes against default behavior), and I also have a workaround solution that uses a local size & setSize function + a useEffect to follow the incremental page increases without triggering useSWRInfinite's default functionality for loading the entirety of the undefined dataset

hopefully this helps too!

  const { size, setSize, data } = useSWRInfinite(...)
  const [localSize, setLocalSize] = useState(1)
  // my pagination function uses setLocalSize instead of setSize

  useEffect(() => {
    if (localSize > size) {
      const isFullyLoaded = data?.length === size
      if (isFullyLoaded) {
        setSize(size + 1)
      }
    }
  }, [localSize, size, data?.length])