Skip to content

Commit

Permalink
Keep all fields with one single key (vercel#1863)
Browse files Browse the repository at this point in the history
* store all data fields using one single key

* fix tests

* fix bugs and tests

* fix lint error

* refactor code

* get rid of $ and $

* simplify serialize
  • Loading branch information
shuding authored and himanshiLt committed Jun 10, 2022
1 parent 4703ed1 commit eb74660
Show file tree
Hide file tree
Showing 12 changed files with 197 additions and 155 deletions.
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

0 comments on commit eb74660

Please sign in to comment.