Skip to content

Commit

Permalink
replace isFallback with isLoading state; optimize code (#1928)
Browse files Browse the repository at this point in the history
  • Loading branch information
shuding committed Apr 16, 2022
1 parent 40a61fd commit 0f552d8
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 108 deletions.
4 changes: 2 additions & 2 deletions infinite/index.ts
Expand Up @@ -267,8 +267,8 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>
get isValidating() {
return swr.isValidating
},
get isFallback() {
return swr.isFallback
get isLoading() {
return swr.isLoading
}
} as SWRInfiniteResponse<Data, Error>
}) as unknown as Middleware
Expand Down
7 changes: 5 additions & 2 deletions src/types.ts
@@ -1,4 +1,5 @@
import * as revalidateEvents from './constants'
import { defaultConfig } from './utils/config'

export type FetcherResponse<Data = unknown> = Data | Promise<Data>
export type BareFetcher<Data = unknown> = (
Expand Down Expand Up @@ -121,7 +122,8 @@ export type Middleware = (
) => <Data = any, Error = any>(
key: Key,
fetcher: BareFetcher<Data> | null,
config: SWRConfiguration<Data, Error, BareFetcher<Data>>
config: typeof defaultConfig &
SWRConfiguration<Data, Error, BareFetcher<Data>>
) => SWRResponse<Data, Error>

type ArgumentsTuple = [any, ...unknown[]] | readonly [any, ...unknown[]]
Expand Down Expand Up @@ -164,6 +166,7 @@ export type State<Data, Error> = {
data?: Data
error?: Error
isValidating?: boolean
isLoading?: boolean
}

export type MutatorFn<Data = any> = (
Expand Down Expand Up @@ -220,7 +223,7 @@ export interface SWRResponse<Data = any, Error = any> {
error: Error | undefined
mutate: KeyedMutator<Data>
isValidating: boolean
isFallback: boolean
isLoading: boolean
}

export type KeyLoader<Args extends Arguments = Arguments> =
Expand Down
115 changes: 52 additions & 63 deletions src/use-swr.ts
Expand Up @@ -87,10 +87,10 @@ export const useSWRHandler = <Data = any, Error = any>(
const getConfig = () => configRef.current
const isActive = () => getConfig().isVisible() && getConfig().isOnline()

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

// Get the current state that SWR should return.
const cached = get()
const cached = getCache()
const cachedData = cached.data
const fallback = isUndefined(fallbackData)
? config.fallback[key]
Expand All @@ -103,7 +103,7 @@ export const useSWRHandler = <Data = any, Error = any>(
// - Suspense mode and there's stale data for the initial render.
// - Not suspense mode and there is no fallback data and `revalidateIfStale` is enabled.
// - `revalidateIfStale` is enabled but `data` is not defined.
const shouldRevalidate = () => {
const shouldDoInitialRevalidation = (() => {
// If `revalidateOnMount` is set, we take the value directly.
if (isInitialMount && !isUndefined(revalidateOnMount))
return revalidateOnMount
Expand All @@ -119,22 +119,24 @@ export const useSWRHandler = <Data = any, Error = any>(
// If there is no stale data, we need to revalidate on mount;
// If `revalidateIfStale` is set to true, we will always revalidate.
return isUndefined(data) || config.revalidateIfStale
}

// Resolve the current validating state.
const resolveValidating = () => {
if (!key || !fetcher) return false
if (cached.isValidating) return true

// If it's not mounted yet and it should revalidate on mount, revalidate.
return isInitialMount && shouldRevalidate()
}
const isValidating = resolveValidating()
})()

// Resolve the default validating state:
// If it's able to validate, and it should revalidate on mount, this will be true.
const defaultValidatingState = !!(
key &&
fetcher &&
isInitialMount &&
shouldDoInitialRevalidation
)
const isValidating = cached.isValidating || defaultValidatingState
const isLoading = cached.isLoading || defaultValidatingState

const currentState = {
data,
error,
isValidating
isValidating,
isLoading
}
const [stateRef, stateDependencies, setState] = useStateWithDeps(currentState)

Expand Down Expand Up @@ -171,6 +173,18 @@ export const useSWRHandler = <Data = any, Error = any>(
key === keyRef.current &&
initialMountedRef.current

// The final state object when request finishes.
const finalState: State<Data, Error> = {
isValidating: false,
isLoading: false
}
const finishRequestAndUpdateState = () => {
setCache(finalState)
// We can only set state if it's safe (still mounted with the same key).
if (isCurrentKeyMounted()) {
setState(finalState)
}
}
const cleanupState = () => {
// Check if it's still the same request before deleting.
const requestInfo = FETCH[key]
Expand All @@ -179,19 +193,15 @@ export const useSWRHandler = <Data = any, Error = any>(
}
}

// The new state object when request finishes.
const newState: State<Data, Error> = { isValidating: false }
const finishRequestAndUpdateState = () => {
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.
set({ isValidating: true })
setState({ isValidating: true })
const initialState: State<Data, Error> = { isValidating: true }
// It is in the `isLoading` state, if and only if there is no cached data.
// This bypasses fallback data and laggy data.
if (isUndefined(getCache().data)) {
initialState.isLoading = true
}
setCache(initialState)
setState(initialState)

try {
if (shouldStartNewRequest) {
Expand All @@ -203,7 +213,7 @@ export const useSWRHandler = <Data = any, Error = any>(

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

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

// If there're other mutations(s), overlapped with the current revalidation:
// case 1:
Expand Down Expand Up @@ -283,19 +292,13 @@ export const useSWRHandler = <Data = any, Error = any>(
// Deep compare with latest state to avoid extra re-renders.
// For local state, compare and assign.
if (!compare(stateRef.current.data, newData)) {
newState.data = newData
finalState.data = newData
} else {
// data and newData are deeply equal
// it should be safe to broadcast the stale data
newState.data = stateRef.current.data
// `data` and `newData` are deeply equal (serialized value).
// So it should be safe to broadcast the stale data to keep referential equality (===).
finalState.data = stateRef.current.data
// At the end of this function, `broadcastState` invokes the `onStateUpdate` function,
// which takes care of avoiding the re-render
}

// For global state, it's possible that the key has changed.
// https://github.com/vercel/swr/pull/1058
if (!compare(get().data, newData)) {
set({ data: newData })
// which takes care of avoiding the re-render.
}

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

// Error event and retry logic. Only for the actual request, not
// deduped ones.
Expand Down Expand Up @@ -354,7 +356,7 @@ 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, isValidating: false })
broadcastState(cache, key, finalState)
}

return true
Expand Down Expand Up @@ -396,7 +398,6 @@ export const useSWRHandler = <Data = any, Error = any>(
useIsomorphicLayoutEffect(() => {
if (!key) return

const keyChanged = key !== keyRef.current
const softRevalidate = revalidate.bind(UNDEFINED, WITH_DEDUPE)

// Expose state updater to global event listeners. So we can update hook's
Expand Down Expand Up @@ -451,18 +452,8 @@ export const useSWRHandler = <Data = any, Error = any>(
keyRef.current = key
initialMountedRef.current = true

// When `key` updates, reset the state to the initial value
// and trigger a rerender if necessary.
if (keyChanged) {
setState({
data,
error,
isValidating
})
}

// Trigger a revalidation.
if (shouldRevalidate()) {
if (shouldDoInitialRevalidation) {
if (isUndefined(data) || IS_SERVER) {
// Revalidate immediately.
softRevalidate()
Expand All @@ -480,7 +471,7 @@ export const useSWRHandler = <Data = any, Error = any>(
unsubUpdate()
unsubEvents()
}
}, [key, revalidate])
}, [key])

// Polling
useIsomorphicLayoutEffect(() => {
Expand Down Expand Up @@ -524,7 +515,7 @@ export const useSWRHandler = <Data = any, Error = any>(
timer = -1
}
}
}, [refreshInterval, refreshWhenHidden, refreshWhenOffline, revalidate])
}, [refreshInterval, refreshWhenHidden, refreshWhenOffline, key])

// Display debug info in React DevTools.
useDebugValue(data)
Expand Down Expand Up @@ -555,11 +546,9 @@ export const useSWRHandler = <Data = any, Error = any>(
stateDependencies.isValidating = true
return isValidating
},
get isFallback() {
stateDependencies.data = true
// `isFallback` is only true when we are displaying a value other than
// the cached one.
return data !== cachedData
get isLoading() {
stateDependencies.isLoading = true
return isLoading
}
} as SWRResponse<Data, Error>
}
Expand Down
4 changes: 2 additions & 2 deletions src/utils/env.ts
@@ -1,7 +1,7 @@
import { useEffect, useLayoutEffect } from 'react'
import { hasRequestAnimationFrame, hasWindow } from './helper'
import { hasRequestAnimationFrame, isWindowDefined } from './helper'

export const IS_SERVER = !hasWindow() || 'Deno' in window
export const IS_SERVER = !isWindowDefined || 'Deno' in window

// Polyfill requestAnimationFrame
export const rAF = (f: (...args: any[]) => void) =>
Expand Down
8 changes: 4 additions & 4 deletions src/utils/helper.ts
@@ -1,7 +1,7 @@
export const noop = () => {}

// Using noop() as the undefined value as undefined can possibly be replaced
// by something else. Prettier ignore and extra parentheses are necessary here
// by something else. Prettier ignore and extra parentheses are necessary here
// to ensure that tsc doesn't remove the __NOINLINE__ comment.
// prettier-ignore
export const UNDEFINED = (/*#__NOINLINE__*/ noop()) as undefined
Expand All @@ -15,7 +15,7 @@ export const mergeObjects = (a: any, b: any) => OBJECT.assign({}, a, b)
const STR_UNDEFINED = 'undefined'

// NOTE: Use function to guarantee it's re-evaluated between jsdom and node runtime for tests.
export const hasWindow = () => typeof window != STR_UNDEFINED
export const hasDocument = () => typeof document != STR_UNDEFINED
export const isWindowDefined = typeof window != STR_UNDEFINED
export const isDocumentDefined = typeof document != STR_UNDEFINED
export const hasRequestAnimationFrame = () =>
hasWindow() && typeof window['requestAnimationFrame'] != STR_UNDEFINED
isWindowDefined && typeof window['requestAnimationFrame'] != STR_UNDEFINED
34 changes: 15 additions & 19 deletions src/utils/web-preset.ts
@@ -1,5 +1,5 @@
import { ProviderConfiguration } from '../types'
import { isUndefined, noop, hasWindow, hasDocument } from './helper'
import { isUndefined, noop, isWindowDefined, isDocumentDefined } from './helper'

/**
* Due to bug https://bugs.chromium.org/p/chromium/issues/detail?id=678075,
Expand All @@ -11,34 +11,30 @@ import { isUndefined, noop, hasWindow, hasDocument } from './helper'
let online = true
const isOnline = () => online

const hasWin = hasWindow()
const hasDoc = hasDocument()

// For node and React Native, `add/removeEventListener` doesn't exist on window.
const onWindowEvent =
hasWin && window.addEventListener
? window.addEventListener.bind(window)
: noop
const onDocumentEvent = hasDoc ? document.addEventListener.bind(document) : noop
const offWindowEvent =
hasWin && window.removeEventListener
? window.removeEventListener.bind(window)
: noop
const offDocumentEvent = hasDoc
? document.removeEventListener.bind(document)
: noop
const [onWindowEvent, offWindowEvent] =
isWindowDefined && window.addEventListener
? [
window.addEventListener.bind(window),
window.removeEventListener.bind(window)
]
: [noop, noop]

const isVisible = () => {
const visibilityState = hasDoc && document.visibilityState
const visibilityState = isDocumentDefined && document.visibilityState
return isUndefined(visibilityState) || visibilityState !== 'hidden'
}

const initFocus = (callback: () => void) => {
// focus revalidate
onDocumentEvent('visibilitychange', callback)
if (isDocumentDefined) {
document.addEventListener('visibilitychange', callback)
}
onWindowEvent('focus', callback)
return () => {
offDocumentEvent('visibilitychange', callback)
if (isDocumentDefined) {
document.removeEventListener('visibilitychange', callback)
}
offWindowEvent('focus', callback)
}
}
Expand Down
4 changes: 2 additions & 2 deletions test/use-swr-cache.test.tsx
Expand Up @@ -198,13 +198,13 @@ describe('useSWR - cache provider', () => {
it('should support fallback values with custom provider', async () => {
const key = createKey()
function Page() {
const { data, isFallback } = useSWR(key, async () => {
const { data, isLoading } = useSWR(key, async () => {
await sleep(10)
return 'data'
})
return (
<>
{String(data)},{String(isFallback)}
{String(data)},{String(isLoading)}
</>
)
}
Expand Down

0 comments on commit 0f552d8

Please sign in to comment.