Skip to content

Code optimizations #1150

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 13 commits into from
May 5, 2021
Merged
Show file tree
Hide file tree
Changes from 11 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
19 changes: 4 additions & 15 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { dequal } from 'dequal/lite'

import { wrapCache } from './cache'
import webPreset from './libs/web-preset'
import { slowConnection } from './env'
import { Configuration, RevalidatorOptions, Revalidator } from './types'

const fetcher = (url: string) => fetch(url).then(res => res.json())
Expand All @@ -15,7 +16,7 @@ function onErrorRetry(
revalidate: Revalidator,
opts: Required<RevalidatorOptions>
): void {
if (!webPreset.isDocumentVisible()) {
if (!webPreset.isVisible()) {
// if it's hidden, stop
// it will auto revalidate when focus
return
Expand All @@ -35,18 +36,6 @@ function onErrorRetry(
setTimeout(revalidate, timeout, opts)
}

// client side: need to adjust the config
// based on the browser status
// slow connection (<= 70Kbps)
const slowConnection =
typeof window !== 'undefined' &&
// @ts-ignore
navigator['connection'] &&
// @ts-ignore
(['slow-2g', '2g'].indexOf(navigator['connection'].effectiveType) !== -1 ||
// @ts-ignore
navigator['connection'].saveData)

// config
const defaultConfig = {
// events
Expand All @@ -64,10 +53,10 @@ const defaultConfig = {
suspense: false,

// timeouts
errorRetryInterval: (slowConnection ? 10 : 5) * 1000,
errorRetryInterval: slowConnection ? 10000 : 5000,
focusThrottleInterval: 5 * 1000,
dedupingInterval: 2 * 1000,
loadingTimeout: (slowConnection ? 5 : 3) * 1000,
loadingTimeout: slowConnection ? 5000 : 3000,
refreshInterval: 0,

// providers
Expand Down
20 changes: 17 additions & 3 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,30 @@ import { useEffect, useLayoutEffect } from 'react'
export const IS_SERVER =
typeof window === 'undefined' ||
// @ts-ignore
!!(typeof Deno !== 'undefined' && Deno && Deno.version && Deno.version.deno)
!!(typeof Deno !== 'undefined' && Deno.version && Deno.version.deno)

const __requestAnimationFrame = !IS_SERVER
? window['requestAnimationFrame']
: null

// polyfill for requestAnimationFrame
export const rAF = IS_SERVER
? null
: window['requestAnimationFrame']
? (f: FrameRequestCallback) => window['requestAnimationFrame'](f)
: __requestAnimationFrame
? (f: FrameRequestCallback) => __requestAnimationFrame(f)
: (f: (...args: any[]) => void) => setTimeout(f, 1)

// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser.
export const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect

// client side: need to adjust the config
// based on the browser status
// slow connection (<= 70Kbps)
export const slowConnection =
!IS_SERVER &&
// @ts-ignore
navigator['connection'] &&
// @ts-ignore
['slow-2g', '2g'].indexOf(navigator['connection'].effectiveType) !== -1
17 changes: 6 additions & 11 deletions src/libs/hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,18 @@ export default function hash(args: any[]): string {
if (!args.length) return ''
let key = 'arg'
for (let i = 0; i < args.length; ++i) {
if (args[i] === null) {
key += '@null'
continue
}
let _hash
if (typeof args[i] !== 'object' && typeof args[i] !== 'function') {
if (
args[i] === null ||
(typeof args[i] !== 'object' && typeof args[i] !== 'function')
) {
// need to consider the case that args[i] is a string:
// args[i] _hash
// "undefined" -> '"undefined"'
// undefined -> 'undefined'
// 123 -> '123'
// "null" -> '"null"'
if (typeof args[i] === 'string') {
_hash = '"' + args[i] + '"'
} else {
_hash = String(args[i])
}
// null -> 'null'
_hash = JSON.stringify(args[i])
} else {
if (!table.has(args[i])) {
_hash = counter
Expand Down
3 changes: 3 additions & 0 deletions src/libs/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// `undefined` can possibly be replaced by something else.
export const UNDEFINED: undefined = ({} as any)[0]
export const isUndefined = (v: any) => v === UNDEFINED
2 changes: 1 addition & 1 deletion src/libs/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function serialize(key: Key): [string, any, string, string] {
args = key
key = hash(key)
} else {
// convert null to ''
// convert falsy values to ''
key = String(key || '')
}

Expand Down
15 changes: 10 additions & 5 deletions src/libs/web-preset.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isUndefined } from './helper'

/**
* Due to bug https://bugs.chromium.org/p/chromium/issues/detail?id=678075,
* it's not reliable to detect if the browser is currently online or offline
Expand All @@ -16,9 +18,12 @@ const addDocumentEventListener =
? document.addEventListener.bind(document)
: null

const isDocumentVisible = () => {
if (addDocumentEventListener && document.visibilityState !== undefined) {
return document.visibilityState !== 'hidden'
const isVisible = () => {
if (addDocumentEventListener) {
const visibilityState = document.visibilityState
if (!isUndefined(visibilityState)) {
return visibilityState !== 'hidden'
}
}
// always assume it's visible
return true
Expand Down Expand Up @@ -47,7 +52,7 @@ const registerOnReconnect = (cb: () => void) => {

export default {
isOnline,
isDocumentVisible,
isVisible,
registerOnFocus,
registerOnReconnect
}
} as const
13 changes: 6 additions & 7 deletions src/resolve-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,15 @@ export default function useArgs<KeyType, ConfigType, Data>(
| readonly [KeyType, ConfigType | undefined]
| readonly [KeyType, Fetcher<Data> | null, ConfigType | undefined]
): [KeyType, Fetcher<Data> | null, (typeof defaultConfig) & ConfigType] {
const config = Object.assign(
{},
defaultConfig,
useContext(SWRConfigContext),
args.length > 2
const config = {
...defaultConfig,
...useContext(SWRConfigContext),
...(args.length > 2
? args[2]
: args.length === 2 && typeof args[1] === 'object'
? args[1]
: {}
) as (typeof defaultConfig) & ConfigType
: {})
} as (typeof defaultConfig) & ConfigType

// In TypeScript `args.length > 2` is not same as `args.lenth === 3`.
// We do a safe type assertion here.
Expand Down
14 changes: 8 additions & 6 deletions src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,7 @@ export default function useStateWithDeps<Data, Error, S = State<Data, Error>>(
(payload: S) => void
] {
const rerender = useState<object>({})[1]

const stateRef = useRef(state)
useIsomorphicLayoutEffect(() => {
stateRef.current = state
})

// If a state property (data, error or isValidating) is accessed by the render
// function, we mark the property as a dependency so if it is updated again
Expand Down Expand Up @@ -55,17 +51,18 @@ export default function useStateWithDeps<Data, Error, S = State<Data, Error>>(
(payload: S) => {
let shouldRerender = false

const currentState = stateRef.current
for (const _ of Object.keys(payload)) {
// Type casting to work around the `for...in` loop
// https://github.com/Microsoft/TypeScript/issues/3500
const k = _ as keyof S & StateKeys

// If the property hasn't changed, skip
if (stateRef.current[k] === payload[k]) {
if (currentState[k] === payload[k]) {
continue
}

stateRef.current[k] = payload[k]
currentState[k] = payload[k]

// If the property is accessed by the component, a rerender should be
// triggered.
Expand All @@ -83,5 +80,10 @@ export default function useStateWithDeps<Data, Error, S = State<Data, Error>>(
[]
)

// Always update the state reference.
useIsomorphicLayoutEffect(() => {
stateRef.current = state
})

return [stateRef, stateDependenciesRef, setState]
}
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export interface Configuration<

export interface Preset {
isOnline: () => boolean
isDocumentVisible: () => boolean
isVisible: () => boolean
registerOnFocus?: (cb: () => void) => void
registerOnReconnect?: (cb: () => void) => void
}
Expand Down
22 changes: 10 additions & 12 deletions src/use-swr-infinite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useRef, useState, useCallback } from 'react'

import { useIsomorphicLayoutEffect } from './env'
import { serialize } from './libs/serialize'
import { isUndefined, UNDEFINED } from './libs/helper'
import useArgs from './resolve-args'
import useSWR from './use-swr'

Expand Down Expand Up @@ -33,13 +34,12 @@ function useSWRInfinite<Data = any, Error = any>(
SWRInfiniteConfiguration<Data, Error>,
Data
>(args)
const cache = config.cache

const {
cache,
initialSize = 1,
revalidateAll = false,
persistSize = false,
...extraConfig
persistSize = false
} = config

// get the serialized key of the first page
Expand Down Expand Up @@ -68,7 +68,7 @@ function useSWRInfinite<Data = any, Error = any>(

const resolvePageSize = useCallback((): number => {
const cachedPageSize = cache.get(pageSizeCacheKey)
return typeof cachedPageSize !== 'undefined' ? cachedPageSize : initialSize
return isUndefined(cachedPageSize) ? initialSize : cachedPageSize

// `cache` isn't allowed to change during the lifecycle
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down Expand Up @@ -132,10 +132,8 @@ function useSWRInfinite<Data = any, Error = any>(
const shouldFetchPage =
revalidateAll ||
force ||
typeof pageData === 'undefined' ||
(typeof force === 'undefined' &&
i === 0 &&
typeof dataRef.current !== 'undefined') ||
isUndefined(pageData) ||
(isUndefined(force) && i === 0 && !isUndefined(dataRef.current)) ||
(originalData && !config.compare(originalData[i], pageData))

if (fn && shouldFetchPage) {
Expand All @@ -157,7 +155,7 @@ function useSWRInfinite<Data = any, Error = any>(
// return the data
return data
},
extraConfig
config
)

// update dataRef
Expand All @@ -168,9 +166,9 @@ function useSWRInfinite<Data = any, Error = any>(
const mutate = useCallback(
(data: MutatorCallback, shouldRevalidate = true) => {
// It is possible that the key is still falsy.
if (!contextCacheKey) return undefined
if (!contextCacheKey) return UNDEFINED

if (shouldRevalidate && typeof data !== 'undefined') {
if (shouldRevalidate && !isUndefined(data)) {
// we only revalidate the pages that are changed
const originalData = dataRef.current
cache.set(contextCacheKey, { data: originalData, force: false })
Expand All @@ -190,7 +188,7 @@ function useSWRInfinite<Data = any, Error = any>(
const setSize = useCallback(
(arg: number | ((size: number) => number)) => {
// It is possible that the key is still falsy.
if (!pageSizeCacheKey) return undefined
if (!pageSizeCacheKey) return UNDEFINED

let size
if (typeof arg === 'function') {
Expand Down
Loading