Skip to content

Refactor code structure #1350

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

Merged
merged 6 commits into from
Aug 19, 2021
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
},
"rules": {
"func-names": [2, "as-needed"],
"no-shadow": 2,
"no-shadow": 0,
"@typescript-eslint/no-shadow": 2,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/no-use-before-define": 0,
"@typescript-eslint/ban-ts-ignore": 0,
Expand Down
29 changes: 21 additions & 8 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,6 @@ export type Middleware = (useSWRNext: SWRHook) => SWRHookWithMiddleware

export type ValueKey = string | any[] | null

export type Updater<Data = any, Error = any> = (
shouldRevalidate?: boolean,
data?: Data,
error?: Error,
shouldDedupe?: boolean,
dedupe?: boolean
) => boolean | Promise<boolean>

export type MutatorCallback<Data = any> = (
currentValue?: Data
) => Promise<undefined | Data> | undefined | Data
Expand Down Expand Up @@ -167,6 +159,27 @@ export type Revalidator = (
revalidateOpts?: RevalidatorOptions
) => Promise<boolean> | void

export const enum RevalidateEvent {
FOCUS_EVENT = 0,
RECONNECT_EVENT = 1,
MUTATE_EVENT = 2
}

type RevalidateCallbackReturnType = {
[RevalidateEvent.FOCUS_EVENT]: void
[RevalidateEvent.RECONNECT_EVENT]: void
[RevalidateEvent.MUTATE_EVENT]: Promise<boolean>
}
export type RevalidateCallback = <K extends RevalidateEvent>(
type: K
) => RevalidateCallbackReturnType[K]

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

export interface Cache<Data = any> {
get(key: Key): Data | null | undefined
set(key: Key, value: Data): void
Expand Down
175 changes: 79 additions & 96 deletions src/use-swr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { isUndefined, UNDEFINED } from './utils/helper'
import ConfigProvider from './utils/config-context'
import useStateWithDeps from './utils/state'
import withArgs from './utils/resolve-args'
import { subscribeCallback } from './utils/subscribe-key'
import {
State,
Broadcaster,
Expand All @@ -15,16 +16,18 @@ import {
MutatorCallback,
SWRResponse,
RevalidatorOptions,
Updater,
Configuration,
SWRConfiguration,
Cache,
ScopedMutator,
SWRHook,
Revalidator,
StateUpdateCallback,
RevalidateEvent,
ProviderOptions
} from './types'

const WITH_DEDUPE = { dedupe: true }

// Generate strictly increasing timestamps.
let __timestamp = 0

Expand All @@ -36,17 +39,27 @@ const broadcastState: Broadcaster = (
isValidating,
shouldRevalidate = false
) => {
const [, , CACHE_REVALIDATORS] = SWRGlobalState.get(cache) as GlobalState
const updaters = CACHE_REVALIDATORS[key]
const promises = []
const [EVENT_REVALIDATORS, STATE_UPDATERS] = SWRGlobalState.get(
cache
) as GlobalState
const revalidators = EVENT_REVALIDATORS[key]
const updaters = STATE_UPDATERS[key]

// Always update states of all hooks.
if (updaters) {
for (let i = 0; i < updaters.length; ++i) {
promises.push(
updaters[i](shouldRevalidate, data, error, isValidating, i > 0)
)
updaters[i](data, error, isValidating)
}
}
return Promise.all(promises).then(() => cache.get(key))

// If we also need to revalidate, only do it for the first hook.
if (shouldRevalidate && revalidators && revalidators[0]) {
return revalidators[0](RevalidateEvent.MUTATE_EVENT).then(() =>
cache.get(key)
)
}

return Promise.resolve(cache.get(key))
}

const internalMutate = async <Data>(
Expand All @@ -58,7 +71,7 @@ const internalMutate = async <Data>(
const [key, , keyErr] = serialize(_key)
if (!key) return UNDEFINED

const [, , , MUTATION_TS, MUTATION_END_TS] = SWRGlobalState.get(
const [, , MUTATION_TS, MUTATION_END_TS] = SWRGlobalState.get(
cache
) as GlobalState

Expand Down Expand Up @@ -137,31 +150,6 @@ const internalMutate = async <Data>(
})
}

// Add a callback function to a list of keyed revalidation functions and returns
// the unregister function.
const addRevalidator = (
revalidators: Record<string, (Revalidator | Updater<any>)[]>,
key: string,
callback: Revalidator | Updater<any>
) => {
if (!revalidators[key]) {
revalidators[key] = [callback]
} else {
revalidators[key].push(callback)
}

return () => {
const keyedRevalidators = revalidators[key]
const index = keyedRevalidators.indexOf(callback)

if (index >= 0) {
// O(1): faster than splice
keyedRevalidators[index] = keyedRevalidators[keyedRevalidators.length - 1]
keyedRevalidators.pop()
}
}
}

export const useSWRHandler = <Data = any, Error = any>(
_key: Key,
fn: Fetcher<Data> | null,
Expand All @@ -180,26 +168,29 @@ export const useSWRHandler = <Data = any, Error = any>(
} = config

const [
FOCUS_REVALIDATORS,
RECONNECT_REVALIDATORS,
CACHE_REVALIDATORS,
EVENT_REVALIDATORS,
STATE_UPDATERS,
MUTATION_TS,
MUTATION_END_TS,
CONCURRENT_PROMISES,
CONCURRENT_PROMISES_TS
] = SWRGlobalState.get(cache) as GlobalState

// `key` is the identifier of the SWR `data` state.
// `keyErr` and `keyValidating` are identifiers of `error` and `isValidating`
// which are derived from `key`.
// `fnArgs` is a list of arguments for `fn`.
// `key` is the identifier of the SWR `data` state, `keyErr` and
// `keyValidating` are identifiers of `error` and `isValidating`,
// all of them are derived from `_key`.
// `fnArgs` is an array of arguments parsed from the key, which will be passed
// to the fetcher.
const [key, fnArgs, keyErr, keyValidating] = serialize(_key)

// If it's the first render of this hook.
// If it's the initial render of this hook.
const initialMountedRef = useRef(false)

// If the hook is unmounted already. This will be used to prevent some effects
// to be called after unmounting.
const unmountedRef = useRef(false)

// The ref to trace the current key.
// Refs to keep the key and config.
const keyRef = useRef(key)
const configRef = useRef(config)

Expand Down Expand Up @@ -252,7 +243,7 @@ export const useSWRHandler = <Data = any, Error = any>(
let startAt: number
let loading = true
const { retryCount, dedupe } = revalidateOpts || {}
const shouldDeduping = !isUndefined(CONCURRENT_PROMISES[key]) && dedupe
const shouldDedupe = !isUndefined(CONCURRENT_PROMISES[key]) && dedupe

// Do unmount check for callbacks:
// If key has changed during the revalidation, or the component has been
Expand All @@ -269,7 +260,7 @@ export const useSWRHandler = <Data = any, Error = any>(
setState({
isValidating: true
})
if (!shouldDeduping) {
if (!shouldDedupe) {
// also update other hooks
broadcastState(
cache,
Expand All @@ -280,7 +271,7 @@ export const useSWRHandler = <Data = any, Error = any>(
)
}

if (shouldDeduping) {
if (shouldDedupe) {
// There's already an ongoing request, this one needs to be
// deduplicated.
startAt = CONCURRENT_PROMISES_TS[key]
Expand Down Expand Up @@ -375,7 +366,7 @@ export const useSWRHandler = <Data = any, Error = any>(
// merge the new state
setState(newState)

if (!shouldDeduping) {
if (!shouldDedupe) {
// also update other hooks
broadcastState(cache, key, newData, newState.error, false)
}
Expand All @@ -389,18 +380,16 @@ export const useSWRHandler = <Data = any, Error = any>(
return false
}

// get a new error
// don't use deep equal for errors
// Get a new error, don't use deep comparison for errors.
cache.set(keyErr, err)

if (stateRef.current.error !== err) {
// we keep the stale data
// Keep the stale data but update error.
setState({
isValidating: false,
error: err
})
if (!shouldDeduping) {
// also broadcast to update other hooks
if (!shouldDedupe) {
// Broadcast to update the states of other hooks.
broadcastState(cache, key, UNDEFINED, err, false)
}
}
Expand Down Expand Up @@ -435,13 +424,11 @@ export const useSWRHandler = <Data = any, Error = any>(
[key]
)

// `mutate`, but bound to the current key.
// Similar to the global mutate, but bound to the current cache and key.
// `cache` isn't allowed to change during the lifecycle.
// eslint-disable-next-line react-hooks/exhaustive-deps
const boundMutate: SWRResponse<Data, Error>['mutate'] = useCallback(
(newData, shouldRevalidate) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix lint

return internalMutate(cache, keyRef.current, newData, shouldRevalidate)
},
// `cache` isn't allowed to change during the lifecycle
// eslint-disable-next-line react-hooks/exhaustive-deps
internalMutate.bind(UNDEFINED, cache, keyRef.current),
[]
)

Expand All @@ -456,38 +443,17 @@ export const useSWRHandler = <Data = any, Error = any>(

// Not the initial render.
const keyChanged = initialMountedRef.current
const softRevalidate = () => revalidate({ dedupe: true })
const softRevalidate = revalidate.bind(UNDEFINED, WITH_DEDUPE)

const isActive = () =>
configRef.current.isVisible() && configRef.current.isOnline()

// Add event listeners.
let nextFocusRevalidatedAt = 0
const onFocus = () => {
const now = Date.now()
if (
configRef.current.revalidateOnFocus &&
now > nextFocusRevalidatedAt &&
isActive()
) {
nextFocusRevalidatedAt = now + configRef.current.focusThrottleInterval
softRevalidate()
}
}

const onReconnect: Revalidator = () => {
if (configRef.current.revalidateOnReconnect && isActive()) {
softRevalidate()
}
}

// Register global cache update listener.
const onUpdate: Updater<Data, Error> = (
shouldRevalidate = true,
// 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,
dedupe = true
updatedIsValidating
) => {
setState({
error: updatedError,
Expand All @@ -499,16 +465,34 @@ export const useSWRHandler = <Data = any, Error = any>(
}
: null)
})
}

if (shouldRevalidate) {
return (dedupe ? softRevalidate : revalidate)()
// Expose revalidators to global event listeners. So we can trigger
// revalidation from the outside.
let nextFocusRevalidatedAt = 0
const onRevalidate = (type: RevalidateEvent) => {
if (type === RevalidateEvent.FOCUS_EVENT) {
const now = Date.now()
if (
configRef.current.revalidateOnFocus &&
now > nextFocusRevalidatedAt &&
isActive()
) {
nextFocusRevalidatedAt = now + configRef.current.focusThrottleInterval
softRevalidate()
}
} else if (type === RevalidateEvent.RECONNECT_EVENT) {
if (configRef.current.revalidateOnReconnect && isActive()) {
softRevalidate()
}
} else if (type === RevalidateEvent.MUTATE_EVENT) {
return revalidate()
}
return false
return UNDEFINED
}

const unsubFocus = addRevalidator(FOCUS_REVALIDATORS, key, onFocus)
const unsubReconn = addRevalidator(RECONNECT_REVALIDATORS, key, onReconnect)
const unsubUpdate = addRevalidator(CACHE_REVALIDATORS, key, onUpdate)
const unsubUpdate = subscribeCallback(key, STATE_UPDATERS, onStateUpdate)
const unsubEvents = subscribeCallback(key, EVENT_REVALIDATORS, onRevalidate)

// Mark the component as mounted and update corresponding refs.
unmountedRef.current = false
Expand Down Expand Up @@ -543,9 +527,8 @@ export const useSWRHandler = <Data = any, Error = any>(
// Mark it as unmounted.
unmountedRef.current = true

unsubFocus()
unsubReconn()
unsubUpdate()
unsubEvents()
}
}, [key, revalidate])

Expand All @@ -570,7 +553,7 @@ export const useSWRHandler = <Data = any, Error = any>(
(refreshWhenHidden || config.isVisible()) &&
(refreshWhenOffline || config.isOnline())
) {
revalidate({ dedupe: true }).then(() => next())
revalidate(WITH_DEDUPE).then(() => next())
} else {
// Schedule next interval to check again.
next()
Expand All @@ -595,7 +578,7 @@ export const useSWRHandler = <Data = any, Error = any>(
// If there is no `error`, the `revalidation` promise needs to be thrown to
// the suspense boundary.
if (suspense && isUndefined(data)) {
throw isUndefined(error) ? revalidate({ dedupe: true }) : error
throw isUndefined(error) ? revalidate(WITH_DEDUPE) : error
}

return Object.defineProperties(
Expand Down
Loading