Skip to content

Commit

Permalink
refactor: switch to useSyncExternalStoreWithSelector (#1953)
Browse files Browse the repository at this point in the history
* type: extends useConfig cache interface (#1938)

* remove mount check for react18 (#1927)

reactwg/react-18#82

related pr #787 #433

* lint: dont check unused vars with underscore prefix (#1939)

* test: upgrade to jest 28 (#1942)

* Upgrade to jest 28

* Upgrade to jest 28

* feat: useSyncExternalStoreWithSelector

* refactor: remove stateUpdate and boardcast

state update should be handled by uSESW

* type: fix test type error

* remove pnpm.lock

* fix: import cjs for codesanbox

* refactor: add selector

* refactor: add cachestate interface and try fix custom cache

* fix: custom cache init

* refactor: remove useless flag

* chore: codesanbox ci

* refactor: remove force render in infinite

* build: add _internal

* chore: mark warning test

* fix: dts generation

* codesanbox ci

* chore: rename swr folder to core

Co-authored-by: Jiachi Liu <[email protected]>
  • Loading branch information
promer94 and huozhi authored May 13, 2022
1 parent 0223e0d commit b8114e6
Show file tree
Hide file tree
Showing 56 changed files with 1,512 additions and 8,248 deletions.
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 SWRCacheResult<Data = any, Error = any> {
data?: Data
error?: Error
isValidating?: boolean
isLoading?: boolean
}

export interface StateDependencies {
data?: boolean
error?: boolean
isValidating?: boolean
isLoading?: boolean
}
134 changes: 56 additions & 78 deletions src/utils/cache.ts → _internal/utils/cache.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { defaultConfigOptions } from './web-preset'
import { IS_SERVER } from './env'
import { UNDEFINED, mergeObjects, noop, isUndefined } from './helper'
import { UNDEFINED, mergeObjects, noop } from './helper'
import { internalMutate } from './mutate'
import { GlobalState, SWRGlobalState } from './global-state'
import { SWRGlobalState } from './global-state'
import * as revalidateEvents from '../constants'

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

const revalidateAllKeys = (
revalidators: Record<string, RevalidateCallback[]>,
Expand All @@ -28,7 +27,7 @@ export const initCache = <Data = any>(
provider: Cache<Data>,
options?: Partial<ProviderConfiguration>
):
| [Cache<Data>, ScopedMutator<Data>, () => void]
| [Cache<Data>, ScopedMutator<Data>, () => void, () => void]
| [Cache<Data>, ScopedMutator<Data>]
| undefined => {
// The global state for a specific provider will be used to deduplicate
Expand Down Expand Up @@ -65,7 +64,6 @@ export const initCache = <Data = any>(
}
const setter = (key: string, value: any, prev: any) => {
provider.set(key, value)

const subs = subscriptions[key]
if (subs) {
for (let i = subs.length; i--; ) {
Expand All @@ -74,85 +72,65 @@ export const initCache = <Data = any>(
}
}

// Update the state if it's new, or the provider has been extended.
SWRGlobalState.set(provider, [
EVENT_REVALIDATORS,
{},
{},
{},
mutate,
setter,
subscribe
])

// This is a new provider, we need to initialize it and setup DOM events
// listeners for `focus` and `reconnect` actions.
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 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
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)
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, unmount]
return [provider, mutate, initProvider, unmount]
}

return [provider, (SWRGlobalState.get(provider) as GlobalState)[4]]
}

const EMPTY_CACHE = {}
export const createCacheHelper = <Data = any, ExtendedInfo = {}>(
cache: Cache,
key: Key
) => {
const state = SWRGlobalState.get(cache) as GlobalState
return [
// Getter
() => cache.get(key) || EMPTY_CACHE,
// Setter
(
info: Partial<
{ data: Data; error: any; isValidating: boolean } | ExtendedInfo
>
) => {
const prev = cache.get(key)
state[5](key as string, mergeObjects(prev, info), prev || EMPTY_CACHE)
},
// Subscriber
state[6]
] as const
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
6 changes: 1 addition & 5 deletions src/utils/config.ts → _internal/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,7 @@ const compare = (currentData: any, newData: any) =>
stableHash(currentData) == stableHash(newData)

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

// Default config
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, SWRCacheResult, 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 = SWRCacheResult<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.
Loading

0 comments on commit b8114e6

Please sign in to comment.