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 #1945

Closed
wants to merge 7 commits into from
Closed
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
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"no-shadow": 0,
"@typescript-eslint/no-shadow": 2,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/no-unused-vars": [0, {"argsIgnorePattern": "^_"}],
"@typescript-eslint/no-use-before-define": 0,
"@typescript-eslint/ban-ts-ignore": 0,
"@typescript-eslint/no-empty-function": 0,
Expand Down
File renamed without changes.
24 changes: 24 additions & 0 deletions _internal/index.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "..",
"outDir": "./dist"
},
"include": ["./**/*.ts"]
}
39 changes: 29 additions & 10 deletions src/types.ts → _internal/types.ts
Original file line number Diff line number Diff line change
@@ -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 All @@ -17,8 +25,8 @@ export type Fetcher<
: never

// Configuration types that are only used internally, not exposed to the user.
export interface InternalConfiguration {
cache: Cache
export interface InternalConfiguration<T extends Cache = Cache> {
cache: T
mutate: ScopedMutator
}

Expand Down Expand Up @@ -77,7 +85,8 @@ export interface PublicConfiguration<
isVisible: () => boolean
}

export type FullConfiguration = InternalConfiguration & PublicConfiguration
export type FullConfiguration<T extends Cache = Cache> =
InternalConfiguration<T> & PublicConfiguration

export type ProviderConfiguration = {
initFocus: (callback: () => void) => (() => void) | void
Expand Down Expand Up @@ -143,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 @@ -254,14 +265,22 @@ export type RevalidateCallback = <K extends RevalidateEvent>(
type: K
) => RevalidateCallbackReturnType[K]

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

export interface Cache<Data = any> {
get(key: Key): Data | null | undefined
set(key: Key, value: Data): void
delete(key: Key): void
}

export interface CacheValue<Data = any, Error = any> {
data?: Data
error?: Error
isValidating?: boolean
isLoading?: boolean
}

export interface StateDependencies {
data?: boolean
error?: boolean
isValidating?: boolean
isLoading?: boolean
}
136 changes: 136 additions & 0 deletions _internal/utils/cache.ts
Original file line number Diff line number Diff line change
@@ -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]]
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
3 changes: 2 additions & 1 deletion src/utils/env.ts → _internal/utils/env.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { useEffect, useLayoutEffect } from 'react'
import { hasRequestAnimationFrame, isWindowDefined } from './helper'

export const IS_REACT_LEGACY = !(React as any).useId
export const IS_REACT_LEGACY = !React.useId

export const IS_SERVER = !isWindowDefined || 'Deno' in window

// Polyfill requestAnimationFrame
Expand Down
4 changes: 4 additions & 0 deletions _internal/utils/global-state.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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.