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

Add isLoading state and refactor the core #1928

Merged
merged 1 commit into from Apr 16, 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
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