Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support multiple useSWRInfinite hooks in a page #1009

Merged
merged 19 commits into from
Mar 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useEffect, useLayoutEffect } from 'react'

export const IS_SERVER =
typeof window === 'undefined' ||
// @ts-ignore
!!(typeof Deno !== 'undefined' && Deno && Deno.version && Deno.version.deno)

// polyfill for requestAnimationFrame
export const rAF = IS_SERVER
? null
: window['requestAnimationFrame'] || (f => setTimeout(f, 1))

// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser.
export const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect
55 changes: 33 additions & 22 deletions src/use-swr-infinite.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// TODO: use @ts-expect-error
import { useContext, useRef, useState, useEffect, useCallback } from 'react'
import { useContext, useRef, useState, useCallback } from 'react'

import defaultConfig, { cache } from './config'
import { useIsomorphicLayoutEffect } from './env'
import SWRConfigContext from './swr-config-context'
import useSWR from './use-swr'

Expand Down Expand Up @@ -77,25 +78,31 @@ function useSWRInfinite<Data = any, Error = any>(
contextCacheKey = 'ctx@' + firstPageKey
}

// page count is cached as well, so when navigating the list can be restored
let pageCountCacheKey: string | null = null
let cachedPageSize
// page size is also cached to share the page data between hooks having the same key
let pageSizeCacheKey: string | null = null
if (firstPageKey) {
pageCountCacheKey = 'len@' + firstPageKey
cachedPageSize = cache.get(pageCountCacheKey)
pageSizeCacheKey = 'len@' + firstPageKey
}
const pageCountRef = useRef<number>(cachedPageSize || initialSize)
const didMountRef = useRef<boolean>(false)

const resolvePageSize = useCallback((): number => {
const cachedPageSize = cache.get(pageSizeCacheKey)
return typeof cachedPageSize !== 'undefined' ? cachedPageSize : initialSize
}, [pageSizeCacheKey, initialSize])
// keep the last page size to restore it with the persistSize option
const lastPageSizeRef = useRef<number>(resolvePageSize())

// every time the key changes, we reset the page size if it's not persisted
useEffect(() => {
if (didMountRef.current) {
if (!persistSize) {
pageCountRef.current = initialSize
}
} else {
useIsomorphicLayoutEffect(() => {
if (!didMountRef.current) {
didMountRef.current = true
return
}
// If the key has been changed, we keep the current page size if persistSize is enabled
cache.set(
pageSizeCacheKey,
persistSize ? lastPageSizeRef.current : initialSize
)
// initialSize isn't allowed to change during the lifecycle
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [firstPageKey])
Expand All @@ -113,8 +120,9 @@ function useSWRInfinite<Data = any, Error = any>(
// return an array of page data
const data: Data[] = []

const pageSize = resolvePageSize()
let previousPageData = null
for (let i = 0; i < pageCountRef.current; ++i) {
for (let i = 0; i < pageSize; ++i) {
const [pageKey, pageArgs] = cache.serializeKey(
getKey(i, previousPageData)
)
Expand Down Expand Up @@ -165,7 +173,7 @@ function useSWRInfinite<Data = any, Error = any>(
)

// update dataRef
useEffect(() => {
useIsomorphicLayoutEffect(() => {
dataRef.current = swr.data
}, [swr.data])

Expand All @@ -188,23 +196,26 @@ function useSWRInfinite<Data = any, Error = any>(
)

// extend the SWR API
const size = pageCountRef.current
const setSize = useCallback(
arg => {
(arg: number | ((size: number) => number)) => {
let size
if (typeof arg === 'function') {
pageCountRef.current = arg(pageCountRef.current)
size = arg(resolvePageSize())
} else if (typeof arg === 'number') {
pageCountRef.current = arg
size = arg
}
if (typeof size === 'number') {
cache.set(pageSizeCacheKey, size)
lastPageSizeRef.current = size
}
cache.set(pageCountCacheKey, pageCountRef.current)
rerender({})
return mutate(v => v)
},
[mutate, pageCountCacheKey]
[pageSizeCacheKey, resolvePageSize, mutate]
)

// Use getter functions to avoid unnecessary re-renders caused by triggering all the getters of the returned swr object
const swrInfinite = { size, setSize, mutate }
const swrInfinite = { size: resolvePageSize(), setSize, mutate }
Object.defineProperties(swrInfinite, {
error: {
get: () => swr.error,
Expand Down
23 changes: 2 additions & 21 deletions src/use-swr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@
import {
useCallback,
useContext,
useEffect,
useLayoutEffect,
useState,
useRef,
useMemo,
useDebugValue
} from 'react'

import defaultConfig, { cache } from './config'
import { IS_SERVER, rAF, useIsomorphicLayoutEffect } from './env'
import SWRConfigContext from './swr-config-context'
import {
Action,
Expand All @@ -25,21 +24,6 @@ import {
SWRConfiguration
} from './types'

const IS_SERVER =
typeof window === 'undefined' ||
// @ts-ignore
!!(typeof Deno !== 'undefined' && Deno && Deno.version && Deno.version.deno)

// polyfill for requestAnimationFrame
const rAF = IS_SERVER
? null
: window['requestAnimationFrame'] || (f => setTimeout(f, 1))

// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser.
const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect

type Revalidator = (...args: any[]) => void

// global state managers
Expand Down Expand Up @@ -578,10 +562,7 @@ function useSWR<Data = any, Error = any>(
const softRevalidate = () => revalidate({ dedupe: true })

// trigger a revalidation
if (
isUpdating ||
willRevalidateOnMount()
) {
if (isUpdating || willRevalidateOnMount()) {
if (typeof latestKeyedData !== 'undefined' && !IS_SERVER) {
// delay revalidate if there's cache
// to not block the rendering
Expand Down
2 changes: 1 addition & 1 deletion test/use-swr-config-callbacks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ describe('useSWR - config callbacks', () => {
expect(state).toEqual(null)

// should trigger a loading slow event
await act(() => sleep(LOADING_TIMEOUT))
await act(() => sleep(LOADING_TIMEOUT * 1.5))
screen.getByText('hello, , a')
expect(state).toEqual('a')

Expand Down
2 changes: 1 addition & 1 deletion test/use-swr-configs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { sleep } from './utils'
describe('useSWR - configs', () => {
it('should read the config fallback from the context', async () => {
let value = 0
const INTERVAL = 50
const INTERVAL = 100
const fetcher = () => value++

function Section() {
Expand Down
58 changes: 58 additions & 0 deletions test/use-swr-infinite.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -514,4 +514,62 @@ describe('useSWRInfinite', () => {
await screen.findByText('page-1-1, page-1-2, page-2-1, page-2-2')
expect(requests).toEqual(['/api?page=1', '/api?page=2'])
})

it('should share data between multiple hooks have the same key', async () => {
const dummyResponses = {
'/api?page=1': ['page-1-1', 'page-1-2'],
'/api?page=2': ['page-2-1', 'page-2-2']
}
const useCustomSWRInfinite = () => {
const { data, setSize, size } = useSWRInfinite<string[], string>(
index => {
return [`page-test-11`, `/api?page=${index + 1}`]
},
async (_, index) => {
return dummyResponses[index]
}
)
return {
data: data ? [].concat(...data) : [],
setSize,
size
}
}

const Component = (props: { label: string }) => {
const { data, size, setSize } = useCustomSWRInfinite()
return (
<>
<ul>
{data.map(value => (
<li key={value}>
{props.label}:{value}
</li>
))}
</ul>
<button onClick={() => setSize(size + 1)}>{props.label}:click</button>
</>
)
}

function Page() {
return (
<div>
<Component label="A" />
<Component label="B" />
</div>
)
}
render(<Page />)

// render responses for page=1
await screen.findByText('A:page-1-2')
await screen.findByText('B:page-1-2')

fireEvent.click(screen.getByText('A:click'))

// render responses for page=2
await screen.findByText('A:page-2-2')
await screen.findByText('B:page-2-2')
})
})