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

Keep all fields with one single key #1863

Merged
merged 8 commits into from Apr 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
75 changes: 37 additions & 38 deletions infinite/index.ts
Expand Up @@ -9,10 +9,12 @@ import useSWR, {
Middleware,
BareFetcher
} from 'swr'

import { useIsomorphicLayoutEffect } from '../src/utils/env'
import { serialize } from '../src/utils/serialize'
import { isUndefined, isFunction, UNDEFINED } from '../src/utils/helper'
import { withMiddleware } from '../src/utils/with-middleware'
import { createCacheHelper } from '../src/utils/cache'

import type {
SWRInfiniteConfiguration,
Expand Down Expand Up @@ -52,34 +54,35 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>
revalidateOnMount = false
} = config

// The serialized key of the first page.
let firstPageKey: string | null = null
// The serialized key of the first page. This key will be used to store
// metadata of this SWR infinite hook.
let infiniteKey: string | undefined
try {
firstPageKey = getFirstPageKey(getKey)
infiniteKey = getFirstPageKey(getKey)
if (infiniteKey) infiniteKey = INFINITE_PREFIX + infiniteKey
} catch (err) {
// not ready
// Not ready yet.
}

// We use cache to pass extra info (context) to fetcher so it can be globally
// shared. The key of the context data is based on the first page key.
let contextCacheKey: string | null = null

// Page size is also cached to share the page data between hooks with the
// same key.
let pageSizeCacheKey: string | null = null

if (firstPageKey) {
contextCacheKey = '$ctx$' + firstPageKey
pageSizeCacheKey = '$len$' + firstPageKey
}
const [get, set] = createCacheHelper<
Data,
{
// We use cache to pass extra info (context) to fetcher so it can be globally
// shared. The key of the context data is based on the first page key.
$ctx: [boolean] | [boolean, Data[] | undefined]
// Page size is also cached to share the page data between hooks with the
// same key.
$len: number
}
>(cache, infiniteKey)

const resolvePageSize = useCallback((): number => {
const cachedPageSize = cache.get(pageSizeCacheKey)
const cachedPageSize = get().$len
return isUndefined(cachedPageSize) ? initialSize : cachedPageSize

// `cache` isn't allowed to change during the lifecycle
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pageSizeCacheKey, initialSize])
}, [infiniteKey, initialSize])
// keep the last page size to restore it with the persistSize option
const lastPageSizeRef = useRef<number>(resolvePageSize())

Expand All @@ -90,28 +93,24 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>
return
}

if (firstPageKey) {
if (infiniteKey) {
// If the key has been changed, we keep the current page size if persistSize is enabled
cache.set(
pageSizeCacheKey,
persistSize ? lastPageSizeRef.current : initialSize
)
set({ $len: persistSize ? lastPageSizeRef.current : initialSize })
}

// `initialSize` isn't allowed to change during the lifecycle
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [firstPageKey])
}, [infiniteKey])

// Needs to check didMountRef during mounting, not in the fetcher
const shouldRevalidateOnMount = revalidateOnMount && !didMountRef.current

// Actual SWR hook to load all pages in one fetcher.
const swr = useSWRNext<Data[], Error>(
firstPageKey ? INFINITE_PREFIX + firstPageKey : null,
infiniteKey,
async () => {
// get the revalidate context
const [forceRevalidateAll, originalData] =
cache.get(contextCacheKey) || []
const [forceRevalidateAll, originalData] = get().$ctx || []

// return an array of page data
const data: Data[] = []
Expand All @@ -128,7 +127,7 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>
}

// Get the cached page data.
let pageData = cache.get(pageKey)
let pageData = cache.get(pageKey)?.data

// should fetch (or revalidate) if:
// - `revalidateAll` is enabled
Expand All @@ -149,15 +148,15 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>

if (fn && shouldFetchPage) {
pageData = await fn(pageArg)
cache.set(pageKey, pageData)
cache.set(pageKey, { ...cache.get(pageKey), data: pageData })
}

data.push(pageData)
previousPageData = pageData
}

// once we executed the data fetching based on the context, clear the context
cache.delete(contextCacheKey)
set({ $ctx: UNDEFINED })

// return the data
return data
Expand Down Expand Up @@ -186,24 +185,24 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>
const shouldRevalidate = args[1] !== false

// It is possible that the key is still falsy.
if (!contextCacheKey) return
if (!infiniteKey) return

if (shouldRevalidate) {
if (!isUndefined(data)) {
// We only revalidate the pages that are changed
const originalData = dataRef.current
cache.set(contextCacheKey, [false, originalData])
set({ $ctx: [false, originalData] })
} else {
// Calling `mutate()`, we revalidate all pages
cache.set(contextCacheKey, [true])
set({ $ctx: [true] })
}
}

return args.length ? swr.mutate(data, shouldRevalidate) : swr.mutate()
},
// swr.mutate is always the same reference
// eslint-disable-next-line react-hooks/exhaustive-deps
[contextCacheKey]
[infiniteKey]
)

// Function to load pages data from the cache based on the page size.
Expand All @@ -216,7 +215,7 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>
const [pageKey] = serialize(getKey(i, previousPageData))

// Get the cached page data.
const pageData = pageKey ? cache.get(pageKey) : UNDEFINED
const pageData = pageKey ? cache.get(pageKey)?.data : UNDEFINED

// Return the current data if we can't get it from the cache.
if (isUndefined(pageData)) return dataRef.current
Expand All @@ -233,7 +232,7 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>
const setSize = useCallback(
(arg: number | ((size: number) => number)) => {
// It is possible that the key is still falsy.
if (!pageSizeCacheKey) return
if (!infiniteKey) return

let size
if (isFunction(arg)) {
Expand All @@ -243,14 +242,14 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>
}
if (typeof size != 'number') return

cache.set(pageSizeCacheKey, size)
set({ $len: size })
lastPageSizeRef.current = size
rerender({})
return mutate(resolvePagesFromCache(size))
},
// `cache` and `rerender` isn't allowed to change during the lifecycle
// eslint-disable-next-line react-hooks/exhaustive-deps
[pageSizeCacheKey, resolvePageSize, mutate]
[infiniteKey, resolvePageSize, mutate]
)

// Use getter functions to avoid unnecessary re-renders caused by triggering
Expand Down
File renamed without changes.
10 changes: 5 additions & 5 deletions src/types.ts
@@ -1,4 +1,4 @@
import * as revalidateEvents from './constants/revalidate-events'
import * as revalidateEvents from './constants'

export type FetcherResponse<Data = unknown> = Data | Promise<Data>
export type BareFetcher<Data = unknown> = (
Expand Down Expand Up @@ -248,11 +248,11 @@ export type RevalidateCallback = <K extends RevalidateEvent>(
type: K
) => RevalidateCallbackReturnType[K]

export type StateUpdateCallback<Data = any, Error = any> = (
data?: Data,
error?: Error,
export type StateUpdateCallback<Data = any, Error = any> = (state: {
data?: Data
error?: Error
isValidating?: boolean
) => void
}) => void

export interface Cache<Data = any> {
get(key: Key): Data | null | undefined
Expand Down
69 changes: 30 additions & 39 deletions src/use-swr.ts
Expand Up @@ -17,7 +17,9 @@ import { subscribeCallback } from './utils/subscribe-key'
import { broadcastState } from './utils/broadcast-state'
import { getTimestamp } from './utils/timestamp'
import { internalMutate } from './utils/mutate'
import * as revalidateEvents from './constants/revalidate-events'
import * as revalidateEvents from './constants'
import { createCacheHelper } from './utils/cache'

import {
State,
Fetcher,
Expand Down Expand Up @@ -69,7 +71,7 @@ export const useSWRHandler = <Data = any, Error = any>(
// all of them are derived from `_key`.
// `fnArg` is the argument/arguments parsed from the key, which will be passed
// to the fetcher.
const [key, fnArg, keyInfo] = serialize(_key)
const [key, fnArg] = serialize(_key)

// If it's the initial render of this hook.
const initialMountedRef = useRef(false)
Expand All @@ -84,17 +86,17 @@ export const useSWRHandler = <Data = any, Error = any>(
const configRef = useRef(config)
const getConfig = () => configRef.current
const isActive = () => getConfig().isVisible() && getConfig().isOnline()
const patchFetchInfo = (info: { isValidating?: boolean; error?: any }) =>
cache.set(keyInfo, mergeObjects(cache.get(keyInfo), info))

const [get, set] = createCacheHelper<Data>(cache, key)

// Get the current state that SWR should return.
const cached = cache.get(key)
const cached = get()
const cachedData = cached.data
const fallback = isUndefined(fallbackData)
? config.fallback[key]
: fallbackData
const data = isUndefined(cached) ? fallback : cached
const info = cache.get(keyInfo) || {}
const error = info.error
const data = isUndefined(cachedData) ? fallback : cachedData
const error = cached.error

const isInitialMount = !initialMountedRef.current

Expand Down Expand Up @@ -122,7 +124,7 @@ export const useSWRHandler = <Data = any, Error = any>(
// Resolve the current validating state.
const resolveValidating = () => {
if (!key || !fetcher) return false
if (info.isValidating) return true
if (cached.isValidating) return true

// If it's not mounted yet and it should revalidate on mount, revalidate.
return isInitialMount && shouldRevalidate()
Expand Down Expand Up @@ -182,33 +184,28 @@ export const useSWRHandler = <Data = any, Error = any>(
// The new state object when request finishes.
const newState: State<Data, Error> = { isValidating: false }
const finishRequestAndUpdateState = () => {
patchFetchInfo({ isValidating: false })
set({ isValidating: false })
// We can only set state if it's safe (still mounted with the same key).
if (isCurrentKeyMounted()) {
setState(newState)
}
}

// Start fetching. Change the `isValidating` state, update the cache.
patchFetchInfo({
isValidating: true
})
set({ isValidating: true })
setState({ isValidating: true })

try {
if (shouldStartNewRequest) {
// Tell all other hooks to change the `isValidating` state.
broadcastState(
cache,
key,
stateRef.current.data,
stateRef.current.error,
true
)
broadcastState(cache, key, {
...stateRef.current,
isValidating: true
})

// If no cache being rendered currently (it shows a blank page),
// we trigger the loading slow event.
if (config.loadingTimeout && !cache.get(key)) {
if (config.loadingTimeout && isUndefined(get().data)) {
setTimeout(() => {
if (loading && isCurrentKeyMounted()) {
getConfig().onLoadingSlow(key, config)
Expand Down Expand Up @@ -251,9 +248,7 @@ export const useSWRHandler = <Data = any, Error = any>(
}

// Clear error.
patchFetchInfo({
error: UNDEFINED
})
set({ error: UNDEFINED })
newState.error = UNDEFINED

// If there're other mutations(s), overlapped with the current revalidation:
Expand Down Expand Up @@ -301,8 +296,8 @@ export const useSWRHandler = <Data = any, Error = any>(

// For global state, it's possible that the key has changed.
// https://github.com/vercel/swr/pull/1058
if (!compare(cache.get(key), newData)) {
cache.set(key, newData)
if (!compare(get().data, newData)) {
set({ data: newData })
}

// Trigger the successful callback if it's the original request.
Expand All @@ -317,7 +312,7 @@ export const useSWRHandler = <Data = any, Error = any>(
// Not paused, we continue handling the error. Otherwise discard it.
if (!getConfig().isPaused()) {
// Get a new error, don't use deep comparison for errors.
patchFetchInfo({ error: err })
set({ error: err })
newState.error = err as Error

// Error event and retry logic. Only for the actual request, not
Expand Down Expand Up @@ -353,13 +348,13 @@ export const useSWRHandler = <Data = any, Error = any>(
// Here is the source of the request, need to tell all other hooks to
// update their states.
if (isCurrentKeyMounted() && shouldStartNewRequest) {
broadcastState(cache, key, newState.data, newState.error, false)
broadcastState(cache, key, { ...newState, isValidating: false })
}

return true
},
// `setState` is immutable, and `eventsCallback`, `fnArg`, `keyInfo`,
// and `keyValidating` are depending on `key`, so we can exclude them from
// `setState` is immutable, and `eventsCallback`, `fnArg`, and
// `keyValidating` are depending on `key`, so we can exclude them from
// the deps array.
//
// FIXME:
Expand Down Expand Up @@ -399,23 +394,19 @@ export const useSWRHandler = <Data = any, Error = any>(

// Expose state updater to global event listeners. So we can update hook's
// internal state from the outside.
const onStateUpdate: StateUpdateCallback<Data, Error> = (
updatedData,
updatedError,
updatedIsValidating
) => {
const onStateUpdate: StateUpdateCallback<Data, Error> = (state = {}) => {
setState(
mergeObjects(
{
error: updatedError,
isValidating: updatedIsValidating
error: state.error,
isValidating: state.isValidating
},
// Since `setState` only shallowly compares states, we do a deep
// comparison here.
compare(stateRef.current.data, updatedData)
compare(stateRef.current.data, state.data)
? UNDEFINED
: {
data: updatedData
data: state.data
}
)
)
Expand Down