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

Improved React 18 support #1962

Merged
merged 9 commits into from May 13, 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
File renamed without changes.
24 changes: 24 additions & 0 deletions _internal/index.ts
@@ -0,0 +1,24 @@
import SWRConfig from './utils/config-context'
import * as revalidateEvents from './constants'

export { SWRConfig, revalidateEvents }

export { initCache } from './utils/cache'
export { defaultConfig, cache, mutate, compare } from './utils/config'
export * from './utils/env'
export { SWRGlobalState } from './utils/global-state'
export { stableHash } from './utils/hash'
export * from './utils/helper'
export { mergeConfigs } from './utils/merge-config'
export { internalMutate } from './utils/mutate'
export { normalize } from './utils/normalize-args'
export { withArgs } from './utils/resolve-args'
export { serialize } from './utils/serialize'
export { useStateWithDeps } from './utils/state'
export { subscribeCallback } from './utils/subscribe-key'
export { getTimestamp } from './utils/timestamp'
export { useSWRConfig } from './utils/use-swr-config'
export { preset, defaultConfigOptions } from './utils/web-preset'
export { withMiddleware } from './utils/with-middleware'

export * from './types'
11 changes: 11 additions & 0 deletions _internal/package.json
@@ -0,0 +1,11 @@
{
"name": "swr-internal",
"version": "0.0.1",
"main": "./dist/index.js",
"module": "./dist/index.esm.js",
"types": "./dist/_internal",
"exports": "./dist/index.mjs",
"peerDependencies": {
"react": "*"
}
}
8 changes: 8 additions & 0 deletions _internal/tsconfig.json
@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "..",
"outDir": "./dist"
},
"include": ["./**/*.ts"]
}
36 changes: 28 additions & 8 deletions src/types.ts → _internal/types.ts
@@ -1,6 +1,14 @@
import * as revalidateEvents from './constants'
import { defaultConfig } from './utils/config'

export type GlobalState = [
Record<string, RevalidateCallback[]>, // EVENT_REVALIDATORS
Record<string, [number, number]>, // MUTATION: [ts, end_ts]
Record<string, [any, number]>, // FETCH: [data, ts]
ScopedMutator, // Mutator
(key: string, value: any, prev: any) => void, // Setter
(key: string, callback: (current: any, prev: any) => void) => () => void // Subscriber
]
export type FetcherResponse<Data = unknown> = Data | Promise<Data>
export type BareFetcher<Data = unknown> = (
...args: any[]
Expand Down Expand Up @@ -144,7 +152,9 @@ export type MutatorCallback<Data = any> = (

export type MutatorOptions<Data = any> = {
revalidate?: boolean
populateCache?: boolean | ((result: any, currentData?: Data) => Data)
populateCache?:
| boolean
| ((result: any, currentData: Data | undefined) => Data)
optimisticData?: Data | ((currentData?: Data) => Data)
rollbackOnError?: boolean
}
Expand Down Expand Up @@ -255,12 +265,22 @@ export type RevalidateCallback = <K extends RevalidateEvent>(
type: K
) => RevalidateCallbackReturnType[K]

export type StateUpdateCallback<Data = any, Error = any> = (
state: State<Data, Error>
) => void

export interface Cache<Data = any, Error = any> {
get(key: Key): State<Data, Error> | undefined
set(key: Key, value: State<Data, Error>): void
export interface Cache<Data = any> {
get(key: Key): CacheValue<Data> | undefined
set(key: Key, value: Data): void
Copy link
Member

Choose a reason for hiding this comment

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

The type of set was updated in https://github.com/vercel/swr/pull/1961/files#r872245634 - seems like a regression.

delete(key: Key): void
}

export interface CacheValue<Data = any, Error = any> {
data?: Data
error?: Error
isValidating?: boolean
isLoading?: boolean
}
Comment on lines +274 to +279
Copy link
Member

Choose a reason for hiding this comment

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

CacheValue is identical to State (see https://github.com/vercel/swr/pull/1961/files#r872242431)

swr/_internal/types.ts

Lines 177 to 182 in 9a0328f

export type State<Data, Error> = {
data?: Data
error?: Error
isValidating?: boolean
isLoading?: boolean
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

These two types should be merged


export interface StateDependencies {
data?: boolean
error?: boolean
isValidating?: boolean
isLoading?: boolean
}
136 changes: 136 additions & 0 deletions _internal/utils/cache.ts
@@ -0,0 +1,136 @@
import { defaultConfigOptions } from './web-preset'
import { IS_SERVER } from './env'
import { UNDEFINED, mergeObjects, noop } from './helper'
import { internalMutate } from './mutate'
import { SWRGlobalState } from './global-state'
import * as revalidateEvents from '../constants'

import {
Cache,
ScopedMutator,
RevalidateEvent,
RevalidateCallback,
ProviderConfiguration,
GlobalState
} from '../types'

const revalidateAllKeys = (
revalidators: Record<string, RevalidateCallback[]>,
type: RevalidateEvent
) => {
for (const key in revalidators) {
if (revalidators[key][0]) revalidators[key][0](type)
}
}

export const initCache = <Data = any>(
provider: Cache<Data>,
options?: Partial<ProviderConfiguration>
):
| [Cache<Data>, ScopedMutator<Data>, () => void, () => void]
| [Cache<Data>, ScopedMutator<Data>]
| undefined => {
// The global state for a specific provider will be used to deduplicate
// requests and store listeners. As well as a mutate function that bound to
// the cache.

// Provider's global state might be already initialized. Let's try to get the
// global state associated with the provider first.
if (!SWRGlobalState.has(provider)) {
const opts = mergeObjects(defaultConfigOptions, options)

// If there's no global state bound to the provider, create a new one with the
// new mutate function.
const EVENT_REVALIDATORS = {}
const mutate = internalMutate.bind(
UNDEFINED,
provider
) as ScopedMutator<Data>
let unmount = noop

const subscriptions: Record<string, ((current: any, prev: any) => void)[]> =
{}
const subscribe = (
key: string,
callback: (current: any, prev: any) => void
) => {
const subs = subscriptions[key] || []
subscriptions[key] = subs

subs.push(callback)
return () => {
subs.splice(subs.indexOf(callback), 1)
}
}
const setter = (key: string, value: any, prev: any) => {
provider.set(key, value)
const subs = subscriptions[key]
if (subs) {
for (let i = subs.length; i--; ) {
subs[i](value, prev)
}
}
}

const initProvider = () => {
if (!SWRGlobalState.has(provider)) {
// Update the state if it's new, or the provider has been extended.
SWRGlobalState.set(provider, [
EVENT_REVALIDATORS,
{},
{},
mutate,
setter,
subscribe
])
if (!IS_SERVER) {
// When listening to the native events for auto revalidations,
// we intentionally put a delay (setTimeout) here to make sure they are
// fired after immediate JavaScript executions, which can possibly be
// React's state updates.
// This avoids some unnecessary revalidations such as
// https://github.com/vercel/swr/issues/1680.
const releaseFocus = opts.initFocus(
setTimeout.bind(
UNDEFINED,
revalidateAllKeys.bind(
UNDEFINED,
EVENT_REVALIDATORS,
revalidateEvents.FOCUS_EVENT
)
)
)
const releaseReconnect = opts.initReconnect(
setTimeout.bind(
UNDEFINED,
revalidateAllKeys.bind(
UNDEFINED,
EVENT_REVALIDATORS,
revalidateEvents.RECONNECT_EVENT
)
)
)
unmount = () => {
releaseFocus && releaseFocus()
releaseReconnect && releaseReconnect()
// When un-mounting, we need to remove the cache provider from the state
// storage too because it's a side-effect. Otherwise when re-mounting we
// will not re-register those event listeners.
SWRGlobalState.delete(provider)
}
}
}
}
initProvider()

// This is a new provider, we need to initialize it and setup DOM events
// listeners for `focus` and `reconnect` actions.

// We might want to inject an extra layer on top of `provider` in the future,
// such as key serialization, auto GC, etc.
// For now, it's just a `Map` interface without any modifications.
return [provider, mutate, initProvider, unmount]
}

return [provider, (SWRGlobalState.get(provider) as GlobalState)[3]]
}
Expand Up @@ -50,10 +50,12 @@ const SWRConfig: FC<
}

// Unsubscribe events.
useIsomorphicLayoutEffect(
() => (cacheContext ? cacheContext[2] : UNDEFINED),
[]
)
useIsomorphicLayoutEffect(() => {
if (cacheContext) {
cacheContext[2] && cacheContext[2]()
return cacheContext[3]
}
}, [])

return createElement(
SWRConfigContext.Provider,
Expand Down
14 changes: 6 additions & 8 deletions src/utils/config.ts → _internal/utils/config.ts
Expand Up @@ -37,13 +37,12 @@ const onErrorRetry = (
setTimeout(revalidate, timeout, opts)
}

const compare = (currentData: any, newData: any) =>
stableHash(currentData) == stableHash(newData)

// Default cache provider
const [cache, mutate] = initCache(new Map()) as [
Cache<any>,
ScopedMutator<any>,
() => {}
]
export { cache, mutate }
const [cache, mutate] = initCache(new Map()) as [Cache<any>, ScopedMutator<any>]
export { cache, mutate, compare }

// Default config
export const defaultConfig: FullConfiguration = mergeObjects(
Expand All @@ -68,8 +67,7 @@ export const defaultConfig: FullConfiguration = mergeObjects(
loadingTimeout: slowConnection ? 5000 : 3000,

// providers
compare: (currentData: any, newData: any) =>
stableHash(currentData) == stableHash(newData),
compare,
isPaused: () => false,
cache,
mutate,
Expand Down
2 changes: 1 addition & 1 deletion src/utils/env.ts → _internal/utils/env.ts
@@ -1,7 +1,7 @@
import React, { useEffect, useLayoutEffect } from 'react'
import { hasRequestAnimationFrame, isWindowDefined } from './helper'

// @ts-expect-error TODO: should remove this when the default react version is 18

export const IS_REACT_LEGACY = !React.useId

export const IS_SERVER = !isWindowDefined || 'Deno' in window
Expand Down
4 changes: 4 additions & 0 deletions _internal/utils/global-state.ts
@@ -0,0 +1,4 @@
import { Cache, GlobalState } from '../types'

// Global state used to deduplicate requests and store listeners
export const SWRGlobalState = new WeakMap<Cache, GlobalState>()
File renamed without changes.
22 changes: 22 additions & 0 deletions src/utils/helper.ts → _internal/utils/helper.ts
@@ -1,3 +1,5 @@
import { SWRGlobalState } from './global-state'
import { Key, Cache, CacheValue, GlobalState } from '../types'
export const noop = () => {}

// Using noop() as the undefined value as undefined can possibly be replaced
Expand All @@ -10,6 +12,7 @@ export const OBJECT = Object

export const isUndefined = (v: any): v is undefined => v === UNDEFINED
export const isFunction = (v: any): v is Function => typeof v == 'function'
export const isEmptyCache = (v: any): boolean => v === EMPTY_CACHE
export const mergeObjects = (a: any, b: any) => OBJECT.assign({}, a, b)

const STR_UNDEFINED = 'undefined'
Expand All @@ -19,3 +22,22 @@ export const isWindowDefined = typeof window != STR_UNDEFINED
export const isDocumentDefined = typeof document != STR_UNDEFINED
export const hasRequestAnimationFrame = () =>
isWindowDefined && typeof window['requestAnimationFrame'] != STR_UNDEFINED

const EMPTY_CACHE = {}
export const createCacheHelper = <Data = any, T = CacheValue<Data, any>>(
cache: Cache,
key: Key
) => {
const state = SWRGlobalState.get(cache) as GlobalState
return [
// Getter
() => (cache.get(key) || EMPTY_CACHE) as T,
// Setter
(info: T) => {
const prev = cache.get(key)
state[4](key as string, mergeObjects(prev, info), prev || EMPTY_CACHE)
},
// Subscriber
state[5]
] as const
}
File renamed without changes.