Repository: zeit/swr (ts)

Issue:
Dynamic polling interval
When changing `refreshInterval` dynamically:

```js
const [int, setInt] = useState(0)
const { data } = useSWR('/api', { refreshInterval: int })

return <>
  {data}
  <button onClick={() => setInt(int + 1000)}>update</button>
</>
```

Sometimes the received `data` will not be updated from the data source upon interval changes. Need to refine the interval change logic and remove `refreshInterval` from the `useEffect` deps, and use a ref for it.

Also we need a test to make sure it'll be stable in the future.


Pull request description:
Fix #182
close #182

Patch:
diff --git a/src/use-swr.ts b/src/use-swr.ts
index 9c9e510..1ab5350 100644
--- a/src/use-swr.ts
+++ b/src/use-swr.ts
@@ -382,16 +382,18 @@ function useSWR<Data = any, Error = any>(
     const softRevalidate = () => revalidate({ dedupe: true })
 
     // trigger a revalidation
-    if (
-      typeof latestKeyedData !== 'undefined' &&
-      !IS_SERVER &&
-      window['requestIdleCallback']
-    ) {
-      // delay revalidate if there's cache
-      // to not block the rendering
-      window['requestIdleCallback'](softRevalidate)
-    } else {
-      softRevalidate()
+    if (!config.initialData) {
+      if (
+        typeof latestKeyedData !== 'undefined' &&
+        !IS_SERVER &&
+        window['requestIdleCallback']
+      ) {
+        // delay revalidate if there's cache
+        // to not block the rendering
+        window['requestIdleCallback'](softRevalidate)
+      } else {
+        softRevalidate()
+      }
     }
 
     // whenever the window gets focused, revalidate
@@ -452,27 +454,6 @@ function useSWR<Data = any, Error = any>(
       CACHE_REVALIDATORS[key].push(onUpdate)
     }
 
-    // set up polling
-    let timeout = null
-    if (config.refreshInterval) {
-      const tick = async () => {
-        if (
-          !errorRef.current &&
-          (config.refreshWhenHidden || isDocumentVisible()) &&
-          (!config.refreshWhenOffline && isOnline())
-        ) {
-          // only revalidate when the page is visible
-          // if API request errored, we stop polling in this round
-          // and let the error retry function handle it
-          await softRevalidate()
-        }
-
-        const interval = config.refreshInterval
-        timeout = setTimeout(tick, interval)
-      }
-      timeout = setTimeout(tick, config.refreshInterval)
-    }
-
     // set up reconnecting when the browser regains network connection
     let reconnect = null
     if (config.revalidateOnReconnect) {
@@ -505,15 +486,42 @@ function useSWR<Data = any, Error = any>(
         }
       }
 
-      if (timeout !== null) {
-        clearTimeout(timeout)
-      }
-
       if (reconnect !== null) {
         removeEventListener('online', reconnect)
       }
     }
-  }, [key, config.refreshInterval, revalidate])
+  }, [key, revalidate])
+
+  // set up polling
+  useIsomorphicLayoutEffect(() => {
+    let timer = null
+    const tick = async () => {
+      if (
+        !errorRef.current &&
+        (config.refreshWhenHidden || isDocumentVisible()) &&
+        (!config.refreshWhenOffline && isOnline())
+      ) {
+        // only revalidate when the page is visible
+        // if API request errored, we stop polling in this round
+        // and let the error retry function handle it
+        await revalidate({ dedupe: true })
+      }
+      if (config.refreshInterval) {
+        timer = setTimeout(tick, config.refreshInterval)
+      }
+    }
+    if (config.refreshInterval) {
+      timer = setTimeout(tick, config.refreshInterval)
+    }
+    return () => {
+      if (timer) clearTimeout(timer)
+    }
+  }, [
+    config.refreshInterval,
+    config.refreshWhenHidden,
+    config.refreshWhenOffline,
+    revalidate
+  ])
 
   // suspense
   if (config.suspense) {


Test patch:
diff --git a/test/use-swr.test.tsx b/test/use-swr.test.tsx
index 64b964d..383f384 100644
--- a/test/use-swr.test.tsx
+++ b/test/use-swr.test.tsx
@@ -243,8 +243,10 @@ describe('useSWR', () => {
   })
 
   it('should accept initial data', async () => {
+    const fetcher = jest.fn(() => 'SWR')
+
     function Page() {
-      const { data } = useSWR('initial-data-1', () => 'SWR', {
+      const { data } = useSWR('initial-data-1', fetcher, {
         initialData: 'Initial'
       })
       return <div>hello, {data}</div>
@@ -252,13 +254,10 @@ describe('useSWR', () => {
 
     const { container } = render(<Page />)
 
+    expect(fetcher).not.toBeCalled()
     expect(container.firstChild.textContent).toMatchInlineSnapshot(
       `"hello, Initial"`
     )
-    await waitForDomChange({ container }) // mount
-    expect(container.firstChild.textContent).toMatchInlineSnapshot(
-      `"hello, SWR"`
-    )
   })
 
   it('should set config as second parameter', async () => {
@@ -335,6 +334,60 @@ describe('useSWR - refresh', () => {
     await act(() => new Promise(res => setTimeout(res, 200))) // update
     expect(container.firstChild.textContent).toMatchInlineSnapshot(`"count: 2"`)
   })
+
+  it('should update data upon interval changes', async () => {
+    let count = 0
+    function Page() {
+      const [int, setInt] = React.useState(200)
+      const { data } = useSWR('/api', () => count++, {
+        refreshInterval: int,
+        dedupingInterval: 100
+      })
+      return <div onClick={() => setInt(int + 100)}>count: {data}</div>
+    }
+    const { container } = render(<Page />)
+    expect(container.firstChild.textContent).toMatchInlineSnapshot(`"count: "`)
+
+    await waitForDomChange({ container }) // mount
+    expect(container.firstChild.textContent).toMatchInlineSnapshot(`"count: 0"`)
+    await act(() => {
+      return new Promise(res => setTimeout(res, 210))
+    })
+    expect(container.firstChild.textContent).toMatchInlineSnapshot(`"count: 1"`)
+    await act(() => {
+      return new Promise(res => setTimeout(res, 50))
+    })
+    expect(container.firstChild.textContent).toMatchInlineSnapshot(`"count: 1"`)
+    await act(() => {
+      return new Promise(res => setTimeout(res, 150))
+    })
+    expect(container.firstChild.textContent).toMatchInlineSnapshot(`"count: 2"`)
+    await act(() => {
+      fireEvent.click(container.firstElementChild)
+      // it will clear 200ms timer and setup a new 300ms timer
+      return new Promise(res => setTimeout(res, 200))
+    })
+    expect(container.firstChild.textContent).toMatchInlineSnapshot(`"count: 2"`)
+    await act(() => {
+      return new Promise(res => setTimeout(res, 110))
+    })
+    expect(container.firstChild.textContent).toMatchInlineSnapshot(`"count: 3"`)
+    await act(() => {
+      // wait for new 300ms timer
+      return new Promise(res => setTimeout(res, 310))
+    })
+    expect(container.firstChild.textContent).toMatchInlineSnapshot(`"count: 4"`)
+    await act(() => {
+      fireEvent.click(container.firstElementChild)
+      // it will clear 300ms timer and setup a new 400ms timer
+      return new Promise(res => setTimeout(res, 300))
+    })
+    expect(container.firstChild.textContent).toMatchInlineSnapshot(`"count: 4"`)
+    await act(() => {
+      return new Promise(res => setTimeout(res, 110))
+    })
+    expect(container.firstChild.textContent).toMatchInlineSnapshot(`"count: 5"`)
+  })
 })
 
 describe('useSWR - revalidate', () => {