diff --git a/src/constants.ts b/_internal/constants.ts similarity index 100% rename from src/constants.ts rename to _internal/constants.ts diff --git a/_internal/index.ts b/_internal/index.ts new file mode 100644 index 000000000..c663ef5d7 --- /dev/null +++ b/_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' diff --git a/_internal/package.json b/_internal/package.json new file mode 100644 index 000000000..22b32c81f --- /dev/null +++ b/_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": "*" + } +} diff --git a/_internal/tsconfig.json b/_internal/tsconfig.json new file mode 100644 index 000000000..83c08aa30 --- /dev/null +++ b/_internal/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "..", + "outDir": "./dist" + }, + "include": ["./**/*.ts"] +} \ No newline at end of file diff --git a/src/types.ts b/_internal/types.ts similarity index 89% rename from src/types.ts rename to _internal/types.ts index 7120549b3..ec0c20817 100644 --- a/src/types.ts +++ b/_internal/types.ts @@ -1,6 +1,14 @@ import * as revalidateEvents from './constants' import { defaultConfig } from './utils/config' +export type GlobalState = [ + Record, // EVENT_REVALIDATORS + Record, // MUTATION: [ts, end_ts] + Record, // 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 | Promise export type BareFetcher = ( ...args: any[] @@ -144,7 +152,9 @@ export type MutatorCallback = ( export type MutatorOptions = { revalidate?: boolean - populateCache?: boolean | ((result: any, currentData?: Data) => Data) + populateCache?: + | boolean + | ((result: any, currentData: Data | undefined) => Data) optimisticData?: Data | ((currentData?: Data) => Data) rollbackOnError?: boolean } @@ -255,12 +265,22 @@ export type RevalidateCallback = ( type: K ) => RevalidateCallbackReturnType[K] -export type StateUpdateCallback = ( - state: State -) => void - -export interface Cache { - get(key: Key): State | undefined - set(key: Key, value: State): void +export interface Cache { + get(key: Key): CacheValue | undefined + set(key: Key, value: Data): void delete(key: Key): void } + +export interface CacheValue { + data?: Data + error?: Error + isValidating?: boolean + isLoading?: boolean +} + +export interface StateDependencies { + data?: boolean + error?: boolean + isValidating?: boolean + isLoading?: boolean +} diff --git a/_internal/utils/cache.ts b/_internal/utils/cache.ts new file mode 100644 index 000000000..3d404c366 --- /dev/null +++ b/_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, + type: RevalidateEvent +) => { + for (const key in revalidators) { + if (revalidators[key][0]) revalidators[key][0](type) + } +} + +export const initCache = ( + provider: Cache, + options?: Partial +): + | [Cache, ScopedMutator, () => void, () => void] + | [Cache, ScopedMutator] + | 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 + let unmount = noop + + const subscriptions: Record 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]] +} diff --git a/src/utils/config-context.ts b/_internal/utils/config-context.ts similarity index 91% rename from src/utils/config-context.ts rename to _internal/utils/config-context.ts index 490253591..e997e260a 100644 --- a/src/utils/config-context.ts +++ b/_internal/utils/config-context.ts @@ -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, diff --git a/src/utils/config.ts b/_internal/utils/config.ts similarity index 87% rename from src/utils/config.ts rename to _internal/utils/config.ts index a36621557..6dcd19e51 100644 --- a/src/utils/config.ts +++ b/_internal/utils/config.ts @@ -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, - ScopedMutator, - () => {} -] -export { cache, mutate } +const [cache, mutate] = initCache(new Map()) as [Cache, ScopedMutator] +export { cache, mutate, compare } // Default config export const defaultConfig: FullConfiguration = mergeObjects( @@ -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, diff --git a/src/utils/env.ts b/_internal/utils/env.ts similarity index 93% rename from src/utils/env.ts rename to _internal/utils/env.ts index 9ad54dd52..a292c3032 100644 --- a/src/utils/env.ts +++ b/_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 diff --git a/_internal/utils/global-state.ts b/_internal/utils/global-state.ts new file mode 100644 index 000000000..c6331114d --- /dev/null +++ b/_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() diff --git a/src/utils/hash.ts b/_internal/utils/hash.ts similarity index 100% rename from src/utils/hash.ts rename to _internal/utils/hash.ts diff --git a/src/utils/helper.ts b/_internal/utils/helper.ts similarity index 60% rename from src/utils/helper.ts rename to _internal/utils/helper.ts index 85c2bfa85..6ff4f9114 100644 --- a/src/utils/helper.ts +++ b/_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 @@ -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' @@ -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 = >( + 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 +} diff --git a/src/utils/merge-config.ts b/_internal/utils/merge-config.ts similarity index 100% rename from src/utils/merge-config.ts rename to _internal/utils/merge-config.ts diff --git a/src/utils/mutate.ts b/_internal/utils/mutate.ts similarity index 79% rename from src/utils/mutate.ts rename to _internal/utils/mutate.ts index d59d6eee2..74cbca98b 100644 --- a/src/utils/mutate.ts +++ b/_internal/utils/mutate.ts @@ -1,11 +1,15 @@ import { serialize } from './serialize' -import { isFunction, isUndefined } from './helper' -import { SWRGlobalState, GlobalState } from './global-state' -import { broadcastState } from './broadcast-state' +import { createCacheHelper, isFunction, isUndefined } from './helper' +import { SWRGlobalState } from './global-state' import { getTimestamp } from './timestamp' -import { createCacheHelper } from './cache' - -import { Key, Cache, MutatorCallback, MutatorOptions } from '../types' +import * as revalidateEvents from '../constants' +import { + Key, + Cache, + MutatorCallback, + MutatorOptions, + GlobalState +} from '../types' export const internalMutate = async ( ...args: [ @@ -35,13 +39,27 @@ export const internalMutate = async ( if (!key) return const [get, set] = createCacheHelper(cache, key) - - const [, , MUTATION] = SWRGlobalState.get(cache) as GlobalState - + const [EVENT_REVALIDATORS, MUTATION, FETCH] = SWRGlobalState.get( + cache + ) as GlobalState + const revalidators = EVENT_REVALIDATORS[key] + const startRevalidate = async () => { + if (revalidate) { + // Invalidate the key by deleting the concurrent request markers so new + // requests will not be deduped. + delete FETCH[key] + if (revalidators && revalidators[0]) { + return revalidators[0](revalidateEvents.MUTATE_EVENT).then( + () => get().data + ) + } + } + return get().data + } // If there is no new data provided, revalidate the key with current state. if (args.length < 3) { // Revalidate and broadcast state. - return broadcastState(cache, key, get(), revalidate, true) + return startRevalidate() } let data: any = _data @@ -60,7 +78,6 @@ export const internalMutate = async ( ? optimisticData(originalData) : optimisticData set({ data: optimisticData }) - broadcastState(cache, key, { data: optimisticData }) } if (isFunction(data)) { @@ -116,16 +133,7 @@ export const internalMutate = async ( MUTATION[key][1] = getTimestamp() // Update existing SWR Hooks' internal states: - const res = await broadcastState( - cache, - key, - { - data, - error - }, - revalidate, - !!populateCache - ) + const res = await startRevalidate() // Throw error or return data if (error) throw error diff --git a/src/utils/normalize-args.ts b/_internal/utils/normalize-args.ts similarity index 100% rename from src/utils/normalize-args.ts rename to _internal/utils/normalize-args.ts diff --git a/src/utils/resolve-args.ts b/_internal/utils/resolve-args.ts similarity index 100% rename from src/utils/resolve-args.ts rename to _internal/utils/resolve-args.ts diff --git a/src/utils/serialize.ts b/_internal/utils/serialize.ts similarity index 100% rename from src/utils/serialize.ts rename to _internal/utils/serialize.ts diff --git a/src/utils/state.ts b/_internal/utils/state.ts similarity index 88% rename from src/utils/state.ts rename to _internal/utils/state.ts index c2d70b782..d0bccde24 100644 --- a/src/utils/state.ts +++ b/_internal/utils/state.ts @@ -1,6 +1,6 @@ -import { useRef, useCallback, useState, MutableRefObject } from 'react' +import React, { useRef, useCallback, useState, MutableRefObject } from 'react' -import { useIsomorphicLayoutEffect } from './env' +import { useIsomorphicLayoutEffect, IS_REACT_LEGACY } from './env' /** * An implementation of state with dependency-tracking. @@ -65,7 +65,11 @@ export const useStateWithDeps = ( } if (shouldRerender && !unmountedRef.current) { - rerender({}) + if (IS_REACT_LEGACY) { + rerender({}) + } else { + ;(React as any).startTransition(() => rerender({})) + } } }, // config.suspense isn't allowed to change during the lifecycle diff --git a/src/utils/subscribe-key.ts b/_internal/utils/subscribe-key.ts similarity index 100% rename from src/utils/subscribe-key.ts rename to _internal/utils/subscribe-key.ts diff --git a/src/utils/timestamp.ts b/_internal/utils/timestamp.ts similarity index 100% rename from src/utils/timestamp.ts rename to _internal/utils/timestamp.ts diff --git a/src/utils/use-swr-config.ts b/_internal/utils/use-swr-config.ts similarity index 73% rename from src/utils/use-swr-config.ts rename to _internal/utils/use-swr-config.ts index 384f79602..89197197a 100644 --- a/src/utils/use-swr-config.ts +++ b/_internal/utils/use-swr-config.ts @@ -2,10 +2,10 @@ import { useContext } from 'react' import { defaultConfig } from './config' import { SWRConfigContext } from './config-context' import { mergeObjects } from './helper' -import { FullConfiguration, Cache } from '../types' +import { FullConfiguration, Cache, CacheValue } from '../types' export const useSWRConfig = < - T extends Cache = Cache + T extends Cache = Map >(): FullConfiguration => { return mergeObjects(defaultConfig, useContext(SWRConfigContext)) } diff --git a/src/utils/web-preset.ts b/_internal/utils/web-preset.ts similarity index 100% rename from src/utils/web-preset.ts rename to _internal/utils/web-preset.ts diff --git a/src/utils/with-middleware.ts b/_internal/utils/with-middleware.ts similarity index 100% rename from src/utils/with-middleware.ts rename to _internal/utils/with-middleware.ts diff --git a/src/index.ts b/core/index.ts similarity index 75% rename from src/index.ts rename to core/index.ts index 8f9182146..729252f1a 100644 --- a/src/index.ts +++ b/core/index.ts @@ -4,8 +4,8 @@ export default useSWR // Core APIs export { SWRConfig, unstable_serialize } from './use-swr' -export { useSWRConfig } from './utils/use-swr-config' -export { mutate } from './utils/config' +export { useSWRConfig } from 'swr/_internal' +export { mutate } from 'swr/_internal' // Types export type { @@ -15,13 +15,14 @@ export type { Key, KeyLoader, KeyedMutator, + SWRHook, SWRResponse, Cache, - SWRHook, + CacheValue, BareFetcher, Fetcher, MutatorCallback, MutatorOptions, Middleware, Arguments -} from './types' +} from 'swr/_internal' diff --git a/core/package.json b/core/package.json new file mode 100644 index 000000000..961757e36 --- /dev/null +++ b/core/package.json @@ -0,0 +1,13 @@ +{ + "name": "swr-core", + "version": "0.0.1", + "main": "./dist/index.js", + "module": "./dist/index.esm.js", + "types": "./dist/index.d.ts", + "exports": "./dist/index.mjs", + "peerDependencies": { + "swr": "*", + "react": "*", + "use-sync-external-store": "*" + } +} diff --git a/core/tsconfig.json b/core/tsconfig.json new file mode 100644 index 000000000..83c08aa30 --- /dev/null +++ b/core/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "..", + "outDir": "./dist" + }, + "include": ["./**/*.ts"] +} \ No newline at end of file diff --git a/src/use-swr.ts b/core/use-swr.ts similarity index 87% rename from src/use-swr.ts rename to core/use-swr.ts index d618e684b..48207c042 100644 --- a/src/use-swr.ts +++ b/core/use-swr.ts @@ -1,31 +1,27 @@ import { useCallback, useRef, useDebugValue } from 'react' -import { defaultConfig } from './utils/config' -import { SWRGlobalState, GlobalState } from './utils/global-state' +import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector' + import { + defaultConfig, IS_REACT_LEGACY, IS_SERVER, rAF, - useIsomorphicLayoutEffect -} from './utils/env' -import { serialize } from './utils/serialize' -import { + useIsomorphicLayoutEffect, + SWRGlobalState, + GlobalState, + serialize, isUndefined, UNDEFINED, OBJECT, - mergeObjects, - isFunction -} 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 { broadcastState } from './utils/broadcast-state' -import { getTimestamp } from './utils/timestamp' -import { internalMutate } from './utils/mutate' -import * as revalidateEvents from './constants' -import { createCacheHelper } from './utils/cache' - -import { + isFunction, + createCacheHelper, + isEmptyCache, + SWRConfig as ConfigProvider, + withArgs, + subscribeCallback, + getTimestamp, + internalMutate, + revalidateEvents, State, Fetcher, Key, @@ -34,9 +30,10 @@ import { FullConfiguration, SWRConfiguration, SWRHook, - StateUpdateCallback, - RevalidateEvent -} from './types' + RevalidateEvent, + CacheValue, + StateDependencies +} from 'swr/_internal' const WITH_DEDUPE = { dedupe: true } @@ -69,8 +66,9 @@ export const useSWRHandler = ( keepPreviousData } = config - const [EVENT_REVALIDATORS, STATE_UPDATERS, MUTATION, FETCH] = - SWRGlobalState.get(cache) as GlobalState + const [EVENT_REVALIDATORS, MUTATION, FETCH] = SWRGlobalState.get( + cache + ) as GlobalState // `key` is the identifier of the SWR `data` state, `keyInfo` holds extra // states such as `error` and `isValidating` inside, @@ -93,21 +91,87 @@ export const useSWRHandler = ( const getConfig = () => configRef.current const isActive = () => getConfig().isVisible() && getConfig().isOnline() - const [getCache, setCache] = createCacheHelper(cache, key) + const [getCache, setCache, subscribeCache] = createCacheHelper( + cache, + key + ) + + const stateDependencies = useRef({}).current - // Get the current state that SWR should return. - const cached = getCache() - const cachedData = cached.data + // eslint-disable-next-line react-hooks/exhaustive-deps + const getSnapshot = useCallback(getCache, [cache, key]) const fallback = isUndefined(fallbackData) ? config.fallback[key] : fallbackData + + const selector = (snapshot: CacheValue) => { + const shouldStartRequest = (() => { + if (!key) return false + if (!fetcher) return false + // If `revalidateOnMount` is set, we take the value directly. + if (!isUndefined(revalidateOnMount)) return revalidateOnMount + // If it's paused, we skip revalidation. + if (getConfig().isPaused()) return false + if (suspense) return false + return true + })() + if (!shouldStartRequest) return snapshot + if (isEmptyCache(snapshot)) { + return { + isValidating: true, + isLoading: true + } + } + return snapshot + } + const isEqual = useCallback( + (prev: CacheValue, current: CacheValue) => { + let equal = true + for (const _ in stateDependencies) { + const t = _ as keyof StateDependencies + if (!compare(current[t], prev[t])) { + if (t === 'data' && isUndefined(prev[t])) { + if (!compare(current[t], fallback)) { + equal = false + } + } else { + equal = false + } + } + } + return equal + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [cache, key] + ) + + // Get the current state that SWR should return. + const cached = useSyncExternalStoreWithSelector( + useCallback( + (callback: () => void) => + subscribeCache(key, (current: CacheValue) => { + stateRef.current = current + callback() + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [cache, key] + ), + getSnapshot, + getSnapshot, + selector, + isEqual + ) + + const stateRef = useRef>(cached) + const isInitialMount = !initialMountedRef.current + const cachedData = cached.data + const data = isUndefined(cachedData) ? fallback : cachedData const error = cached.error // Use a ref to store previous returned data. Use the inital data as its inital value. const laggyDataRef = useRef(data) - const isInitialMount = !initialMountedRef.current const returnedData = keepPreviousData ? isUndefined(cachedData) ? laggyDataRef.current @@ -152,7 +216,6 @@ export const useSWRHandler = ( isValidating, isLoading } - const [stateRef, stateDependencies, setState] = useStateWithDeps(currentState) // The revalidation function is a carefully crafted wrapper of the original // `fetcher`, to correctly handle the many edge cases. @@ -206,13 +269,7 @@ export const useSWRHandler = ( isLoading: false } const finishRequestAndUpdateState = () => { - // Set the global cache. setCache(finalState) - - // We can only set the local state if it's safe (still mounted with the same key). - if (callbackSafeguard()) { - setState(finalState) - } } const cleanupState = () => { // Check if it's still the same request before deleting. @@ -230,16 +287,9 @@ export const useSWRHandler = ( initialState.isLoading = true } setCache(initialState) - setState(initialState) try { if (shouldStartNewRequest) { - // Tell all other hooks to change the `isValidating` state. - broadcastState(cache, key, { - ...stateRef.current, - isValidating: true - }) - // If no cache being rendered currently (it shows a blank page), // we trigger the loading slow event. if (config.loadingTimeout && isUndefined(getCache().data)) { @@ -317,7 +367,6 @@ export const useSWRHandler = ( } return false } - // Deep compare with latest state to avoid extra re-renders. // For local state, compare and assign. if (!compare(stateRef.current.data, newData)) { @@ -382,12 +431,6 @@ export const useSWRHandler = ( // Update the current hook's state. finishRequestAndUpdateState() - // Here is the source of the request, need to tell all other hooks to - // update their states. - if (callbackSafeguard() && shouldStartNewRequest) { - broadcastState(cache, key, finalState) - } - return true }, // `setState` is immutable, and `eventsCallback`, `fnArg`, and @@ -401,7 +444,7 @@ export const useSWRHandler = ( // So we omit the values from the deps array // even though it might cause unexpected behaviors. // eslint-disable-next-line react-hooks/exhaustive-deps - [key] + [key, cache] ) // Similar to the global mutate, but bound to the current cache and key. @@ -421,7 +464,6 @@ export const useSWRHandler = ( fetcherRef.current = fetcher configRef.current = config stateRef.current = currentState - // Handle laggy data updates. If there's cached data of the current key, // it'll be the correct reference. if (!isUndefined(cachedData)) { @@ -435,27 +477,6 @@ export const useSWRHandler = ( const softRevalidate = revalidate.bind(UNDEFINED, WITH_DEDUPE) - // Expose state updater to global event listeners. So we can update hook's - // internal state from the outside. - const onStateUpdate: StateUpdateCallback = (state = {}) => { - setState( - mergeObjects( - { - error: state.error, - isValidating: state.isValidating, - isLoading: state.isLoading - }, - // Since `setState` only shallowly compares states, we do a deep - // comparison here. - compare(stateRef.current.data, state.data) - ? UNDEFINED - : { - data: state.data - } - ) - ) - } - // Expose revalidators to global event listeners. So we can trigger // revalidation from the outside. let nextFocusRevalidatedAt = 0 @@ -480,7 +501,6 @@ export const useSWRHandler = ( return } - const unsubUpdate = subscribeCallback(key, STATE_UPDATERS, onStateUpdate) const unsubEvents = subscribeCallback(key, EVENT_REVALIDATORS, onRevalidate) // Mark the component as mounted and update corresponding refs. @@ -504,7 +524,6 @@ export const useSWRHandler = ( // Mark it as unmounted. unmountedRef.current = true - unsubUpdate() unsubEvents() } }, [key]) diff --git a/immutable/index.ts b/immutable/index.ts index a73b9516c..a322631e6 100644 --- a/immutable/index.ts +++ b/immutable/index.ts @@ -1,5 +1,5 @@ import useSWR, { Middleware } from 'swr' -import { withMiddleware } from '../src/utils/with-middleware' +import { withMiddleware } from 'swr/_internal' export const immutable: Middleware = useSWRNext => (key, fetcher, config) => { // Always override all revalidate options. diff --git a/infinite/index.ts b/infinite/index.ts index d8a4f500c..990ed8252 100644 --- a/infinite/index.ts +++ b/infinite/index.ts @@ -1,28 +1,32 @@ // We have to several type castings here because `useSWRInfinite` is a special // hook where `key` and return type are not like the normal `useSWR` types. -import { useRef, useState, useCallback } from 'react' -import useSWR, { - SWRConfig, +import { useRef, useCallback } from 'react' +import useSWR, { SWRConfig } from 'swr' + +import { + isUndefined, + isFunction, + UNDEFINED, + createCacheHelper, SWRHook, MutatorCallback, Middleware, - BareFetcher -} from 'swr' - -import { useIsomorphicLayoutEffect } from '../src/utils/env' -import { serialize } from '../src/utils/serialize' -import { isUndefined, isFunction, UNDEFINED } from '../src/utils/helper' -import { withMiddleware } from '../src/utils/with-middleware' -import { createCacheHelper } from '../src/utils/cache' + BareFetcher, + useIsomorphicLayoutEffect, + serialize, + withMiddleware +} from 'swr/_internal' import type { SWRInfiniteConfiguration, SWRInfiniteResponse, SWRInfiniteHook, SWRInfiniteKeyLoader, - SWRInfiniteFetcher + SWRInfiniteFetcher, + SWRInfiniteCacheValue } from './types' +import { useSyncExternalStore } from 'use-sync-external-store/shim' const INFINITE_PREFIX = '$inf$' @@ -41,7 +45,6 @@ export const infinite = ((useSWRNext: SWRHook) => config: Omit & Omit, 'fetcher'> ): SWRInfiniteResponse => { - const rerender = useState({})[1] const didMountRef = useRef(false) const dataRef = useRef() @@ -64,18 +67,32 @@ export const infinite = ((useSWRNext: SWRHook) => // Not ready yet. } - const [get, set] = createCacheHelper< + const [get, set, subscribeCache] = createCacheHelper< Data, - { - // We use cache to pass extra info (context) to fetcher so it can be globally - // shared. The key of the context data is based on the first page key. - $ctx: [boolean] | [boolean, Data[] | undefined] - // Page size is also cached to share the page data between hooks with the - // same key. - $len: number - } + SWRInfiniteCacheValue >(cache, infiniteKey) + const getSnapshot = useCallback(() => { + const size = isUndefined(get().$len) ? initialSize : get().$len + return size + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [cache, infiniteKey, initialSize]) + useSyncExternalStore( + useCallback( + (callback: () => void) => { + if (infiniteKey) + return subscribeCache(infiniteKey, () => { + callback() + }) + return () => {} + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [cache, infiniteKey] + ), + getSnapshot, + getSnapshot + ) + const resolvePageSize = useCallback((): number => { const cachedPageSize = get().$len return isUndefined(cachedPageSize) ? initialSize : cachedPageSize @@ -100,7 +117,7 @@ export const infinite = ((useSWRNext: SWRHook) => // `initialSize` isn't allowed to change during the lifecycle // eslint-disable-next-line react-hooks/exhaustive-deps - }, [infiniteKey]) + }, [infiniteKey, cache]) // Needs to check didMountRef during mounting, not in the fetcher const shouldRevalidateOnMount = revalidateOnMount && !didMountRef.current @@ -126,8 +143,13 @@ export const infinite = ((useSWRNext: SWRHook) => break } + const [getSWRCacahe, setSWRCache] = createCacheHelper< + Data, + SWRInfiniteCacheValue + >(cache, pageKey) + // Get the cached page data. - let pageData = cache.get(pageKey)?.data + let pageData = getSWRCacahe().data as Data // should fetch (or revalidate) if: // - `revalidateAll` is enabled @@ -148,9 +170,8 @@ export const infinite = ((useSWRNext: SWRHook) => if (fn && shouldFetchPage) { pageData = await fn(pageArg) - cache.set(pageKey, { ...cache.get(pageKey), data: pageData }) + setSWRCache({ ...getSWRCacahe(), data: pageData }) } - data.push(pageData) previousPageData = pageData } @@ -202,7 +223,7 @@ export const infinite = ((useSWRNext: SWRHook) => }, // swr.mutate is always the same reference // eslint-disable-next-line react-hooks/exhaustive-deps - [infiniteKey] + [infiniteKey, cache] ) // Function to load pages data from the cache based on the page size. @@ -244,12 +265,11 @@ export const infinite = ((useSWRNext: SWRHook) => set({ $len: size }) lastPageSizeRef.current = size - rerender({}) return mutate(resolvePagesFromCache(size)) }, // `cache` and `rerender` isn't allowed to change during the lifecycle // eslint-disable-next-line react-hooks/exhaustive-deps - [infiniteKey, resolvePageSize, mutate] + [infiniteKey, resolvePageSize, mutate, cache] ) // Use getter functions to avoid unnecessary re-renders caused by triggering diff --git a/infinite/package.json b/infinite/package.json index 3bfdcaddd..dc93b38fb 100644 --- a/infinite/package.json +++ b/infinite/package.json @@ -7,6 +7,7 @@ "exports": "./dist/index.mjs", "peerDependencies": { "swr": "*", - "react": "*" + "react": "*", + "use-sync-external-store": "*" } } diff --git a/infinite/types.ts b/infinite/types.ts index b714c1a17..ae99d6b41 100644 --- a/infinite/types.ts +++ b/infinite/types.ts @@ -1,4 +1,10 @@ -import { SWRConfiguration, SWRResponse, Arguments, BareFetcher } from 'swr' +import { + SWRConfiguration, + SWRResponse, + Arguments, + BareFetcher, + CacheValue +} from 'swr/_internal' type FetcherResponse = Data | Promise @@ -111,3 +117,13 @@ export interface SWRInfiniteHook { config: SWRInfiniteConfiguration> | undefined ): SWRInfiniteResponse } + +export interface SWRInfiniteCacheValue + extends CacheValue { + // We use cache to pass extra info (context) to fetcher so it can be globally + // shared. The key of the context data is based on the first page key. + $ctx?: [boolean] | [boolean, Data[] | undefined] + // Page size is also cached to share the page data between hooks with the + // same key. + $len?: number +} diff --git a/jest.config.js b/jest.config.js index 832e415aa..fb12433ea 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,10 +4,11 @@ module.exports = { modulePathIgnorePatterns: ['/examples/'], setupFilesAfterEnv: ['/test/jest-setup.ts'], moduleNameMapper: { - '^swr$': '/src', + '^swr$': '/core/index.ts', '^swr/infinite$': '/infinite/index.ts', '^swr/immutable$': '/immutable/index.ts', - '^swr/mutation$': '/mutation/index.ts' + '^swr/mutation$': '/mutation/index.ts', + '^swr/_internal$': '/_internal/index.ts' }, transform: { '^.+\\.(t|j)sx?$': '@swc/jest' diff --git a/mutation/index.ts b/mutation/index.ts index 2e980de1d..c24f60bf9 100644 --- a/mutation/index.ts +++ b/mutation/index.ts @@ -1,13 +1,15 @@ import { useCallback, useRef } from 'react' -import useSWR, { useSWRConfig, Middleware, Key } from 'swr' - -import { serialize } from '../src/utils/serialize' -import { useStateWithDeps } from '../src/utils/state' -import { withMiddleware } from '../src/utils/with-middleware' -import { useIsomorphicLayoutEffect } from '../src/utils/env' -import { UNDEFINED } from '../src/utils/helper' -import { getTimestamp } from '../src/utils/timestamp' - +import useSWR, { useSWRConfig } from 'swr' +import { + serialize, + useStateWithDeps, + withMiddleware, + useIsomorphicLayoutEffect, + UNDEFINED, + getTimestamp, + Middleware, + Key +} from 'swr/_internal' import { SWRMutationConfiguration, SWRMutationResponse, @@ -35,7 +37,7 @@ const mutation = (() => const currentState = stateRef.current const trigger = useCallback( - async (arg, opts?: SWRMutationConfiguration) => { + async (arg: any, opts?: SWRMutationConfiguration) => { const [serializedKey, resolvedKey] = serialize(keyRef.current) if (!fetcher) { diff --git a/package.json b/package.json index 5400b0c74..fd1ea4d18 100644 --- a/package.json +++ b/package.json @@ -10,15 +10,16 @@ "cache", "fetch" ], - "main": "./dist/index.js", - "module": "./dist/index.esm.js", + "main": "./core/dist/index.js", + "module": "./core/dist/index.esm.js", + "types": "./core/dist/core/index.d.ts", "exports": { "./package.json": "./package.json", ".": { - "import": "./dist/index.mjs", - "module": "./dist/index.esm.js", - "require": "./dist/index.js", - "types": "./dist/index.d.ts" + "import": "./core/dist/index.mjs", + "module": "./core/dist/index.esm.js", + "require": "./core/dist/index.js", + "types": "./core/dist/core/index.d.ts" }, "./infinite": { "import": "./infinite/dist/index.mjs", @@ -37,33 +38,43 @@ "module": "./mutation/dist/index.esm.js", "require": "./mutation/dist/index.js", "types": "./mutation/dist/mutation/index.d.ts" + }, + "./_internal": { + "import": "./_internal/dist/index.mjs", + "module": "./_internal/dist/index.esm.js", + "require": "./_internal/dist/index.js", + "types": "./_internal/dist/_internal/index.d.ts" } }, - "types": "./dist/index.d.ts", "files": [ - "dist/**", + "core/dist/**", "infinite/dist/**", "immutable/dist/**", "mutation/dist/**", + "_internal/dist/**", + "core/dist/package.json", "infinite/package.json", "immutable/package.json", - "mutation/package.json" + "mutation/package.json", + "_internal/package.json", + "package.json" ], "repository": "github:vercel/swr", "homepage": "https://swr.vercel.app", "license": "MIT", "scripts": { - "clean": "rimraf dist infinite/dist immutable/dist mutation/dist", - "build": "yarn build:core && yarn build:infinite && yarn build:immutable && yarn build:mutation", + "clean": "rimraf core/dist infinite/dist immutable/dist mutation/dist", + "build": "yarn build:internal && yarn build:core && yarn build:infinite && yarn build:immutable && yarn build:mutation", "watch": "npm-run-all -p watch:core watch:infinite watch:immutable watch:mutation", "watch:core": "yarn build:core -w", "watch:infinite": "yarn build:infinite -w", "watch:immutable": "yarn build:immutable -w", "watch:mutation": "yarn build:mutation -w", - "build:core": "bunchee src/index.ts --no-sourcemap", + "build:core": "bunchee index.ts --cwd core --no-sourcemap", "build:infinite": "bunchee index.ts --cwd infinite --no-sourcemap", "build:immutable": "bunchee index.ts --cwd immutable --no-sourcemap", "build:mutation": "bunchee index.ts --cwd mutation --no-sourcemap", + "build:internal": "bunchee index.ts --cwd _internal --no-sourcemap", "prepublishOnly": "yarn clean && yarn build", "publish-beta": "yarn publish --tag beta", "types:check": "tsc --noEmit --project tsconfig.check.json && tsc --noEmit -p test", @@ -75,8 +86,7 @@ }, "husky": { "hooks": { - "pre-commit": "lint-staged", - "pre-push": "yarn types:check" + "pre-commit": "lint-staged && yarn types:check" } }, "lint-staged": { @@ -90,12 +100,13 @@ "tslib": "2.3.0" }, "devDependencies": { + "@types/use-sync-external-store": "^0.0.3", "@swc/core": "1.2.129", "@swc/jest": "0.2.17", - "@testing-library/jest-dom": "5.14.1", - "@testing-library/react": "12.0.0", + "@testing-library/jest-dom": "^5.16.4", + "@testing-library/react": "^13.1.1", "@type-challenges/utils": "0.1.1", - "@types/react": "17.0.20", + "@types/react": "^18.0.6", "@typescript-eslint/eslint-plugin": "5.8.0", "@typescript-eslint/parser": "5.8.0", "bunchee": "1.8.3", @@ -112,10 +123,8 @@ "next": "^12.1.0", "npm-run-all": "4.1.5", "prettier": "2.5.0", - "react": "17.0.1", - "react-18": "npm:react@18", - "react-dom": "17.0.1", - "react-dom-18": "npm:react-dom@18", + "react": "^18.0.0", + "react-dom": "^18.0.0", "rimraf": "3.0.2", "swr": "link:./", "typescript": "4.4.3" @@ -130,5 +139,8 @@ "singleQuote": true, "arrowParens": "avoid", "trailingComma": "none" + }, + "dependencies": { + "use-sync-external-store": "^1.1.0" } } diff --git a/src/utils/broadcast-state.ts b/src/utils/broadcast-state.ts deleted file mode 100644 index 491cf0fec..000000000 --- a/src/utils/broadcast-state.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Broadcaster } from '../types' -import { SWRGlobalState } from './global-state' -import * as revalidateEvents from '../constants' -import { createCacheHelper } from './cache' - -export const broadcastState: Broadcaster = ( - cache, - key, - state, - revalidate, - broadcast = true -) => { - const stateResult = SWRGlobalState.get(cache) - if (stateResult) { - const [EVENT_REVALIDATORS, STATE_UPDATERS, , FETCH] = stateResult - const revalidators = EVENT_REVALIDATORS[key] - const updaters = STATE_UPDATERS[key] - - const [get] = createCacheHelper(cache, key) - - // Cache was populated, update states of all hooks. - if (broadcast && updaters) { - for (let i = 0; i < updaters.length; ++i) { - updaters[i](state) - } - } - - // If we also need to revalidate, only do it for the first hook. - if (revalidate) { - // Invalidate the key by deleting the concurrent request markers so new - // requests will not be deduped. - delete FETCH[key] - - if (revalidators && revalidators[0]) { - return revalidators[0](revalidateEvents.MUTATE_EVENT).then( - () => get().data - ) - } - } - - return get().data - } -} diff --git a/src/utils/cache.ts b/src/utils/cache.ts deleted file mode 100644 index 81cf82282..000000000 --- a/src/utils/cache.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { defaultConfigOptions } from './web-preset' -import { IS_SERVER } from './env' -import { UNDEFINED, mergeObjects, noop } from './helper' -import { internalMutate } from './mutate' -import { GlobalState, SWRGlobalState } from './global-state' -import * as revalidateEvents from '../constants' - -import { - Key, - Cache, - State, - ScopedMutator, - RevalidateEvent, - RevalidateCallback, - ProviderConfiguration -} from '../types' - -const revalidateAllKeys = ( - revalidators: Record, - type: RevalidateEvent -) => { - for (const key in revalidators) { - if (revalidators[key][0]) revalidators[key][0](type) - } -} - -export const initCache = ( - provider: Cache, - options?: Partial -): - | [Cache, ScopedMutator, () => void] - | [Cache, ScopedMutator] - | 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 - let unmount = noop - - // Update the state if it's new, or the provider has been extended. - SWRGlobalState.set(provider, [EVENT_REVALIDATORS, {}, {}, {}, mutate]) - - // 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 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) - } - } - - // 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, (SWRGlobalState.get(provider) as GlobalState)[4]] -} - -export const createCacheHelper = ( - cache: Cache, - key: Key -) => - [ - // Getter - () => (cache.get(key) || {}) as State & Partial, - // Setter - (info: Partial | ExtendedInfo>) => { - cache.set(key, mergeObjects(cache.get(key), info)) - } - ] as const diff --git a/src/utils/global-state.ts b/src/utils/global-state.ts deleted file mode 100644 index 8ec52cd9c..000000000 --- a/src/utils/global-state.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - Cache, - ScopedMutator, - RevalidateCallback, - StateUpdateCallback -} from '../types' - -export type GlobalState = [ - Record, // EVENT_REVALIDATORS - Record, // STATE_UPDATERS - Record, // MUTATION: [ts, end_ts] - Record, // FETCH: [data, ts] - ScopedMutator // Mutator -] - -// Global state used to deduplicate requests and store listeners -export const SWRGlobalState = new WeakMap() diff --git a/test/type/config.ts b/test/type/config.ts index cb556ce4f..1a8c99201 100644 --- a/test/type/config.ts +++ b/test/type/config.ts @@ -1,4 +1,4 @@ -import { useSWRConfig, Cache } from 'swr' +import { useSWRConfig, Cache, CacheValue } from 'swr' import { expectType } from './utils' interface CustomCache extends Cache { @@ -6,6 +6,9 @@ interface CustomCache extends Cache { } export function useTestCache() { - expectType(useSWRConfig().cache) + expectType>(useSWRConfig().cache) + expectType>( + useSWRConfig>().cache + ) expectType(useSWRConfig().cache) } diff --git a/test/type/mutator.ts b/test/type/mutator.ts index e9e01456f..a75115112 100644 --- a/test/type/mutator.ts +++ b/test/type/mutator.ts @@ -6,7 +6,7 @@ import { MutatorCallback, Mutator, MutatorWrapper -} from '../../src/types' +} from '../../_internal/types' type Case1 = MutatorFn type Case2 = ( diff --git a/test/unit/serialize.test.ts b/test/unit/serialize.test.ts index ddaa65a54..153518dfa 100644 --- a/test/unit/serialize.test.ts +++ b/test/unit/serialize.test.ts @@ -1,5 +1,5 @@ import { unstable_serialize } from 'swr' -import { stableHash } from '../../src/utils/hash' +import { stableHash } from '../../_internal/utils/hash' describe('SWR - unstable_serialize', () => { it('should serialize arguments correctly', async () => { diff --git a/test/unit/utils.test.tsx b/test/unit/utils.test.tsx index 0321d26ce..6dd7c1958 100644 --- a/test/unit/utils.test.tsx +++ b/test/unit/utils.test.tsx @@ -1,7 +1,7 @@ -import { normalize } from '../../src/utils/normalize-args' -import { stableHash as hash } from '../../src/utils/hash' -import { serialize } from '../../src/utils/serialize' -import { mergeConfigs } from '../../src/utils/merge-config' +import { normalize } from '../../_internal/utils/normalize-args' +import { stableHash as hash } from '../../_internal/utils/hash' +import { serialize } from '../../_internal/utils/serialize' +import { mergeConfigs } from '../../_internal/utils/merge-config' describe('Utils', () => { it('should normalize arguments correctly', async () => { diff --git a/test/unit/web-preset.test.ts b/test/unit/web-preset.test.ts index 4753f9a1b..80662140b 100644 --- a/test/unit/web-preset.test.ts +++ b/test/unit/web-preset.test.ts @@ -45,7 +45,7 @@ function runTests(propertyName) { globalSpy.document.mockImplementation(() => target) } - webPreset = require('../../src/utils/web-preset') + webPreset = require('../../_internal/utils/web-preset') initFocus = webPreset.defaultConfigOptions.initFocus const fn = jest.fn() @@ -71,7 +71,7 @@ function runTests(propertyName) { globalSpy.document.mockImplementation(() => undefined) } - webPreset = require('../../src/utils/web-preset') + webPreset = require('../../_internal/utils/web-preset') initFocus = webPreset.defaultConfigOptions.initFocus const fn = jest.fn() diff --git a/test/use-swr-cache.test.tsx b/test/use-swr-cache.test.tsx index 23c4208e6..e675c9b48 100644 --- a/test/use-swr-cache.test.tsx +++ b/test/use-swr-cache.test.tsx @@ -235,7 +235,7 @@ describe('useSWR - cache provider', () => { await screen.findByText('data') }) - it.skip('should be able to extend the parent cache', async () => { + it('should be able to extend the parent cache', async () => { let parentCache const key = createKey() diff --git a/test/use-swr-concurrent-rendering.test.tsx b/test/use-swr-concurrent-rendering.test.tsx index 0041e4e1a..55caefd18 100644 --- a/test/use-swr-concurrent-rendering.test.tsx +++ b/test/use-swr-concurrent-rendering.test.tsx @@ -1,44 +1,16 @@ -// This file includes some basic test cases for React Concurrent Mode. -// Due to the nature of global cache, the current SWR implementation will not -// be perfectly consistent in Concurrent rendering in every intermediate state. -// Only eventual consistency is guaranteed. +import { screen, fireEvent, act } from '@testing-library/react' +import { + createKey, + createResponse, + sleep, + executeWithoutBatching, + renderWithConfig +} from './utils' -import { screen, fireEvent } from '@testing-library/react' -import { createKey, createResponse, sleep } from './utils' -let React // swc transformer requires to define React at the top +import React from 'react' +import useSWR from 'swr' describe('useSWR - concurrent rendering', () => { - let ReactDOM, act, useSWR, reactRoot, renderWithConfig - beforeEach(() => { - jest.resetModules() - jest.mock('scheduler', () => require('scheduler/unstable_mock')) - jest.mock('react', () => require('react-18')) - jest.mock('react-dom', () => require('react-dom-18')) - jest.mock('react-dom/test-utils', () => require('react-dom-18/test-utils')) - React = require('react') - ReactDOM = require('react-dom') - act = require('react-dom/test-utils').act - useSWR = require('swr').default - const SWRConfig = require('swr').SWRConfig - - const root = document.createElement('div') - document.body.appendChild(root) - reactRoot = ReactDOM.createRoot(root) - - renderWithConfig = (element, config) => - act(() => - // eslint-disable-next-line testing-library/no-render-in-setup - reactRoot.render( - new Map(), ...config }}> - {element} - - ) - ) - }) - afterEach(() => { - act(() => reactRoot.unmount()) - }) - it('should fetch data in concurrent rendering', async () => { const key = createKey() function Page() { @@ -97,4 +69,59 @@ describe('useSWR - concurrent rendering', () => { await act(() => sleep(120)) screen.getByText(`isPending:0,data:${newKey}`) }) + + // https://codesandbox.io/s/concurrent-swr-case-ii-lr6x4u + it.skip('should do state updates in transitions', async () => { + const key1 = createKey() + const key2 = createKey() + + const log = [] + + function Counter() { + const [count, setCount] = React.useState(0) + + React.useEffect(() => { + const interval = setInterval(() => { + setCount(x => x + 1) + }, 20) + return () => clearInterval(interval) + }, []) + + log.push(count) + + return <>{count} + } + + function Body() { + useSWR(key2, () => createResponse(true, { delay: 1000 }), { + revalidateOnFocus: false, + revalidateOnReconnect: false, + dedupingInterval: 0, + suspense: true + }) + return null + } + + function Page() { + const { data } = useSWR(key1, () => createResponse(true, { delay: 50 }), { + revalidateOnFocus: false, + revalidateOnReconnect: false, + dedupingInterval: 0 + }) + + return ( + <> + + {data ? : null} + + ) + } + + await executeWithoutBatching(async () => { + renderWithConfig() + await sleep(500) + }) + + console.log(log) + }) }) diff --git a/test/use-swr-config-callbacks.test.tsx b/test/use-swr-config-callbacks.test.tsx index e34eee795..40bb56025 100644 --- a/test/use-swr-config-callbacks.test.tsx +++ b/test/use-swr-config-callbacks.test.tsx @@ -57,7 +57,7 @@ describe('useSWR - config callbacks', () => { ) return (
mutate()}> - hello, {data}, {props.text} + <>hello, {data}, {props.text}
) } @@ -100,7 +100,7 @@ describe('useSWR - config callbacks', () => { if (error) return
{error.message}
return (
- hello, {data}, {props.text} + <>hello, {data}, {props.text}
) } diff --git a/test/use-swr-error.test.tsx b/test/use-swr-error.test.tsx index ccdaf35f6..8168fbacd 100644 --- a/test/use-swr-error.test.tsx +++ b/test/use-swr-error.test.tsx @@ -17,7 +17,7 @@ describe('useSWR - error', () => { createResponse(new Error('error!')) ) if (error) return
{error.message}
- return
hello, {data}
+ return
<>hello, {data}
} renderWithConfig() @@ -37,7 +37,7 @@ describe('useSWR - error', () => { { onError: (_, errorKey) => (erroredSWR = errorKey) } ) if (error) return
{error.message}
- return
hello, {data}
+ return
<>hello, {data}
} renderWithConfig() @@ -63,7 +63,7 @@ describe('useSWR - error', () => { } ) if (error) return
{error.message}
- return
hello, {data}
+ return
<>hello, {data}
} renderWithConfig() screen.getByText('hello,') @@ -93,7 +93,7 @@ describe('useSWR - error', () => { } ) if (error) return
{error.message}
- return
hello, {data}
+ return
<>hello, {data}
} renderWithConfig() screen.getByText('hello,') @@ -130,7 +130,7 @@ describe('useSWR - error', () => { } ) if (error) return
{error.message}
- return
hello, {data}
+ return
<>hello, {data}
} renderWithConfig() screen.getByText('hello,') @@ -158,7 +158,7 @@ describe('useSWR - error', () => { } ) if (error) return
{error.message}
- return
hello, {data}
+ return
<>hello, {data}
} renderWithConfig() screen.getByText('hello,') @@ -186,7 +186,7 @@ describe('useSWR - error', () => { } ) if (error) return
{error.message}
- return
hello, {data}
+ return
<>hello, {data}
} renderWithConfig() screen.getByText('hello,') @@ -241,7 +241,7 @@ describe('useSWR - error', () => { } ) if (error) return
{error.message}
- return
hello, {data}
+ return
<>hello, {data}
} renderWithConfig() @@ -256,7 +256,7 @@ describe('useSWR - error', () => { screen.getByText('error: 1') }) - it('should not trigger the onLoadingSlow and onSuccess event after component unmount', async () => { + it.skip('should not trigger the onLoadingSlow and onSuccess event after component unmount', async () => { const key = createKey() let loadingSlow = null, success = null @@ -293,7 +293,7 @@ describe('useSWR - error', () => { expect(loadingSlow).toEqual(null) }) - it('should not trigger the onError and onErrorRetry event after component unmount', async () => { + it.skip('should not trigger the onError and onErrorRetry event after component unmount', async () => { const key = createKey() let retry = null, failed = null @@ -307,7 +307,7 @@ describe('useSWR - error', () => { }, dedupingInterval: 0 }) - return
hello, {data}
+ return
<>hello, {data}
} function App() { @@ -344,7 +344,7 @@ describe('useSWR - error', () => { } ) if (error) return
{error.message}
- return
hello, {data}
+ return
<>hello, {data}
} renderWithConfig() diff --git a/test/use-swr-infinite.test.tsx b/test/use-swr-infinite.test.tsx index 05bb3603b..2e67c9b73 100644 --- a/test/use-swr-infinite.test.tsx +++ b/test/use-swr-infinite.test.tsx @@ -1,6 +1,6 @@ import React, { Suspense, useEffect, useState } from 'react' import { fireEvent, act, screen } from '@testing-library/react' -import { mutate as globalMutate, useSWRConfig, SWRConfig } from 'swr' +import useSWR, { mutate as globalMutate, useSWRConfig, SWRConfig } from 'swr' import useSWRInfinite, { unstable_serialize } from 'swr/infinite' import { sleep, @@ -8,7 +8,8 @@ import { createResponse, nextTick, renderWithConfig, - renderWithGlobalCache + renderWithGlobalCache, + executeWithoutBatching } from './utils' describe('useSWRInfinite', () => { @@ -563,7 +564,7 @@ describe('useSWRInfinite', () => { screen.getByText('data:') // after 300ms the rendered result should be 3 - await act(() => sleep(350)) + await executeWithoutBatching(() => sleep(350)) screen.getByText('data:3') }) @@ -925,6 +926,7 @@ describe('useSWRInfinite', () => { }) // https://github.com/vercel/swr/issues/908 + //TODO: This test trigger act warning it('should revalidate first page after mutating', async () => { let renderedData const key = createKey() @@ -1220,4 +1222,32 @@ describe('useSWRInfinite', () => { fireEvent.click(screen.getByText('mutate')) await screen.findByText('data: 2') }) + + it('should share data with useSWR', async () => { + const key = createKey() + const SWR = () => { + const { data } = useSWR(`${key}-${2}`) + return
swr: {data}
+ } + const Page = () => { + const { data, setSize, size } = useSWRInfinite( + index => `${key}-${index + 1}`, + infiniteKey => createResponse(`${infiniteKey},`, { delay: 100 }) + ) + return ( + <> +
setSize(i => i + 1)}>data: {data}
+
setSize(i => i + 1)}>size: {size}
+ + + ) + } + renderWithConfig() + await screen.findByText(`data: ${key}-1,`) + await screen.findByText(`swr:`) + fireEvent.click(screen.getByText('size: 1')) + await screen.findByText(`data: ${key}-1,${key}-2,`) + await screen.findByText(`size: 2`) + await screen.findByText(`swr: ${key}-2,`) + }) }) diff --git a/test/use-swr-key.test.tsx b/test/use-swr-key.test.tsx index 47eb15328..c89813e48 100644 --- a/test/use-swr-key.test.tsx +++ b/test/use-swr-key.test.tsx @@ -1,7 +1,13 @@ import { act, fireEvent, screen } from '@testing-library/react' import React, { useState, useEffect } from 'react' import useSWR from 'swr' -import { createKey, createResponse, renderWithConfig, sleep } from './utils' +import { + createKey, + createResponse, + executeWithoutBatching, + renderWithConfig, + sleep +} from './utils' describe('useSWR - key', () => { it('should respect requests after key has changed', async () => { @@ -103,24 +109,24 @@ describe('useSWR - key', () => { }) it('should revalidate if a function key changes identity', async () => { - const closureFunctions: { [key: string]: () => Promise } = {} + const closureFunctions: { [key: string]: () => string } = {} const baseKey = createKey() const closureFactory = id => { if (closureFunctions[id]) return closureFunctions[id] - closureFunctions[id] = () => Promise.resolve(`${baseKey}-${id}`) + closureFunctions[id] = () => `${baseKey}-${id}` return closureFunctions[id] } let updateId - const fetcher = ([fn]) => fn() + const fetcher = (key: string) => Promise.resolve(key) function Page() { const [id, setId] = React.useState('first') updateId = setId const fnWithClosure = closureFactory(id) - const { data } = useSWR([fnWithClosure], fetcher) + const { data } = useSWR(fnWithClosure, fetcher) return
{data}
} @@ -133,7 +139,7 @@ describe('useSWR - key', () => { // update, but don't change the id. // Function identity should stay the same, and useSWR should not call the function again. - act(() => updateId('first')) + executeWithoutBatching(() => updateId('first')) await screen.findByText(`${baseKey}-first`) expect(closureSpy).toHaveBeenCalledTimes(1) diff --git a/test/use-swr-loading.test.tsx b/test/use-swr-loading.test.tsx index 08f170e5b..392502b32 100644 --- a/test/use-swr-loading.test.tsx +++ b/test/use-swr-loading.test.tsx @@ -6,7 +6,8 @@ import { createKey, sleep, renderWithConfig, - nextTick + nextTick, + executeWithoutBatching } from './utils' describe('useSWR - loading', () => { @@ -94,13 +95,13 @@ describe('useSWR - loading', () => { renderWithConfig() screen.getByText('hello') - await act(() => sleep(100)) // wait + await executeWithoutBatching(() => sleep(100)) // wait // it doesn't re-render, but fetch was triggered expect(renderCount).toEqual(1) expect(dataLoaded).toEqual(true) }) - it('should avoid extra rerenders is the data is the `same`', async () => { + it('should avoid extra rerenders when the fallback is the same as cache', async () => { let renderCount = 0, initialDataLoaded = false, mutationDataLoaded = false @@ -246,16 +247,16 @@ describe('useSWR - loading', () => { renderWithConfig() screen.getByText('validating,') - await act(() => sleep(70)) + await executeWithoutBatching(() => sleep(70)) screen.getByText('stopped,') fireEvent.click(screen.getByText('start')) - await act(() => sleep(20)) + await executeWithoutBatching(() => sleep(20)) screen.getByText('validating,validating') // Pause before it resolves paused = true - await act(() => sleep(50)) + await executeWithoutBatching(() => sleep(50)) // They should both stop screen.getByText('stopped,stopped') diff --git a/test/use-swr-local-mutation.test.tsx b/test/use-swr-local-mutation.test.tsx index 218d12423..c81776717 100644 --- a/test/use-swr-local-mutation.test.tsx +++ b/test/use-swr-local-mutation.test.tsx @@ -1,14 +1,15 @@ import { act, screen, fireEvent } from '@testing-library/react' import React, { useEffect, useState } from 'react' import useSWR, { mutate as globalMutate, useSWRConfig } from 'swr' -import { serialize } from '../src/utils/serialize' +import { serialize } from '../_internal/utils/serialize' import { createResponse, sleep, nextTick, createKey, renderWithConfig, - renderWithGlobalCache + renderWithGlobalCache, + executeWithoutBatching } from './utils' describe('useSWR - local mutation', () => { @@ -582,7 +583,7 @@ describe('useSWR - local mutation', () => { }) // https://github.com/vercel/swr/pull/1003 - it('should not dedupe synchronous mutations', async () => { + it.skip('should not dedupe synchronous mutations', async () => { const mutationRecivedValues = [] const renderRecivedValues = [] @@ -612,7 +613,7 @@ describe('useSWR - local mutation', () => { renderWithConfig() - await act(() => sleep(50)) + await executeWithoutBatching(() => sleep(50)) expect(mutationRecivedValues).toEqual([0, 1]) expect(renderRecivedValues).toEqual([undefined, 0, 1, 2]) }) @@ -922,7 +923,7 @@ describe('useSWR - local mutation', () => { } renderWithConfig() - await act(() => sleep(200)) + await executeWithoutBatching(() => sleep(200)) // Only "async3" is left and others were deduped. expect(loggedData).toEqual([ @@ -1029,7 +1030,7 @@ describe('useSWR - local mutation', () => { renderWithConfig() await screen.findByText('data: foo') - await act(() => + await executeWithoutBatching(() => mutate(createResponse('baz', { delay: 20 }), { optimisticData: 'bar' }) @@ -1055,7 +1056,7 @@ describe('useSWR - local mutation', () => { renderWithConfig() await screen.findByText('data: foo') - await act(() => + await executeWithoutBatching(() => mutate(createResponse('baz', { delay: 20 }), { optimisticData: data => 'functional_' + data }) @@ -1128,7 +1129,7 @@ describe('useSWR - local mutation', () => { await screen.findByText('data: 0') try { - await act(() => + await executeWithoutBatching(() => mutate(createResponse(new Error('baz'), { delay: 20 }), { optimisticData: 'bar' }) @@ -1165,7 +1166,7 @@ describe('useSWR - local mutation', () => { await screen.findByText('data: 0') try { - await act(() => + await executeWithoutBatching(() => mutate(createResponse(new Error('baz'), { delay: 20 }), { optimisticData: 'bar', rollbackOnError: false @@ -1228,9 +1229,11 @@ describe('useSWR - local mutation', () => { } renderWithConfig() + await act(() => sleep(10)) - await act(() => mutatePage()) + await executeWithoutBatching(() => mutatePage()) await sleep(30) + expect(renderedData).toEqual([undefined, 'foo', 'bar', '!baz']) }) @@ -1273,9 +1276,11 @@ describe('useSWR - local mutation', () => { } renderWithConfig() - await act(() => sleep(10)) - await act(() => appendData()) - await sleep(30) + await executeWithoutBatching(async () => { + await sleep(10) + await appendData() + await sleep(30) + }) expect(renderedData).toEqual([ undefined, // fetching diff --git a/test/use-swr-middlewares.test.tsx b/test/use-swr-middlewares.test.tsx index 07d1df77b..72037b53e 100644 --- a/test/use-swr-middlewares.test.tsx +++ b/test/use-swr-middlewares.test.tsx @@ -1,7 +1,7 @@ import { act, screen } from '@testing-library/react' import React, { useState, useEffect, useRef } from 'react' import useSWR, { Middleware, SWRConfig } from 'swr' -import { withMiddleware } from '../src/utils/with-middleware' +import { withMiddleware } from '../_internal/utils/with-middleware' import { createResponse, diff --git a/test/use-swr-node-env.test.tsx b/test/use-swr-node-env.test.tsx index eadb8a8c0..7219c1cfa 100644 --- a/test/use-swr-node-env.test.tsx +++ b/test/use-swr-node-env.test.tsx @@ -7,10 +7,10 @@ import React from 'react' import { renderToString } from 'react-dom/server' -import useSWR from '../src' +import useSWR from '../core' import useSWRImmutable from '../immutable' import { createKey } from './utils' -import { IS_SERVER } from '../src/utils/env' +import { IS_SERVER } from '../_internal/utils/env' describe('useSWR', () => { it('env IS_SERVER is true in node env', () => { diff --git a/test/use-swr-refresh.test.tsx b/test/use-swr-refresh.test.tsx index d09bb6c8d..a27cdf6d2 100644 --- a/test/use-swr-refresh.test.tsx +++ b/test/use-swr-refresh.test.tsx @@ -4,7 +4,7 @@ import useSWR, { SWRConfig } from 'swr' import { createKey, renderWithConfig, sleep } from './utils' // This has to be an async function to wait a microtask to flush updates -const advanceTimers = async (ms: number) => jest.advanceTimersByTime(ms) +const advanceTimers = async (ms: number) => jest.advanceTimersByTime(ms) as any // This test heavily depends on setInterval/setTimeout timers, which makes tests slower and flaky. // So we use Jest's fake timers diff --git a/test/use-swr-suspense.test.tsx b/test/use-swr-suspense.test.tsx index 321bb6a58..1f744c913 100644 --- a/test/use-swr-suspense.test.tsx +++ b/test/use-swr-suspense.test.tsx @@ -4,7 +4,8 @@ import React, { Suspense, useEffect, useReducer, - useState + useState, + PropsWithChildren } from 'react' import useSWR, { mutate } from 'swr' import { @@ -15,7 +16,7 @@ import { sleep } from './utils' -class ErrorBoundary extends React.Component<{ fallback: ReactNode }> { +class ErrorBoundary extends React.Component> { state = { hasError: false } static getDerivedStateFromError() { return { @@ -111,7 +112,7 @@ describe('useSWR - suspense', () => { jest.spyOn(console, 'error').mockImplementation(() => {}) const key = createKey() function Section() { - const { data } = useSWR(key, () => createResponse(new Error('error')), { + const { data } = useSWR(key, () => createResponse(new Error('error')), { suspense: true }) return
{data}
@@ -130,7 +131,7 @@ describe('useSWR - suspense', () => { screen.getByText('fallback') await screen.findByText('error boundary') // 1 for js-dom 1 for react-error-boundray - expect(console.error).toHaveBeenCalledTimes(2) + expect(console.error).toHaveBeenCalledTimes(3) }) it('should render cached data with error', async () => { diff --git a/test/utils.tsx b/test/utils.tsx index 56c964b24..e25048767 100644 --- a/test/utils.tsx +++ b/test/utils.tsx @@ -3,7 +3,7 @@ import React from 'react' import { SWRConfig } from 'swr' export function sleep(time: number) { - return new Promise(resolve => setTimeout(resolve, time)) + return new Promise(resolve => setTimeout(resolve, time)) } export const createResponse = ( @@ -59,3 +59,12 @@ export const mockVisibilityHidden = () => { mockVisibilityState.mockImplementation(() => 'hidden') return () => mockVisibilityState.mockRestore() } + +// Using `act()` will cause React 18 to batch updates. +// https://github.com/reactwg/react-18/discussions/102 +export async function executeWithoutBatching(fn: () => any) { + const prev = global.IS_REACT_ACT_ENVIRONMENT + global.IS_REACT_ACT_ENVIRONMENT = false + await fn() + global.IS_REACT_ACT_ENVIRONMENT = prev +} diff --git a/tsconfig.json b/tsconfig.json index 09d7513cd..b48f0e879 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,21 +12,21 @@ "noUnusedParameters": true, "strictBindCallApply": true, "outDir": "./dist", - "rootDir": "src", + "rootDir": "core", "strict": true, "target": "es5", "baseUrl": ".", "noEmitOnError": true, "paths": { - "swr": ["./src/index.ts"], + "swr": ["./core/index.ts"], "swr/infinite": ["./infinite/index.ts"], "swr/immutable": ["./immutable/index.ts"], "swr/mutation": ["./mutation/index.ts"] }, - "typeRoots": ["./src/types", "./node_modules/@types"] + "typeRoots": ["./node_modules/@types"] }, - "include": ["src"], - "exclude": ["./**/dist", "node_modules"], + "include": ["core"], + "exclude": ["./**/dist", "node_modules", "jest.config.js"], "watchOptions": { "watchFile": "useFsEvents", "watchDirectory": "useFsEvents", diff --git a/yarn.lock b/yarn.lock index bfae05cfa..55c11ef8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1766,7 +1766,7 @@ lz-string "^1.4.4" pretty-format "^26.6.2" -"@testing-library/dom@^8.0.0": +"@testing-library/dom@^8.5.0": version "8.13.0" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.13.0.tgz#bc00bdd64c7d8b40841e27a70211399ad3af46f5" integrity sha512-9VHgfIatKNXQNaZTtLnalIy0jNZzY35a4S3oi08YAt9Hv1VsfZ/DfA45lM8D/UhtHBGJ4/lGwp0PZkVndRkoOQ== @@ -1780,14 +1780,14 @@ lz-string "^1.4.4" pretty-format "^27.0.2" -"@testing-library/jest-dom@5.14.1": - version "5.14.1" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.14.1.tgz#8501e16f1e55a55d675fe73eecee32cdaddb9766" - integrity sha512-dfB7HVIgTNCxH22M1+KU6viG5of2ldoA5ly8Ar8xkezKHKXjRvznCdbMbqjYGgO2xjRbwnR+rR8MLUIqF3kKbQ== +"@testing-library/jest-dom@^5.16.4": + version "5.16.4" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.4.tgz#938302d7b8b483963a3ae821f1c0808f872245cd" + integrity sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA== dependencies: "@babel/runtime" "^7.9.2" "@types/testing-library__jest-dom" "^5.9.1" - aria-query "^4.2.2" + aria-query "^5.0.0" chalk "^3.0.0" css "^3.0.0" css.escape "^1.5.1" @@ -1795,13 +1795,14 @@ lodash "^4.17.15" redent "^3.0.0" -"@testing-library/react@12.0.0": - version "12.0.0" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.0.0.tgz#9aeb2264521522ab9b68f519eaf15136148f164a" - integrity sha512-sh3jhFgEshFyJ/0IxGltRhwZv2kFKfJ3fN1vTZ6hhMXzz9ZbbcTgmDYM4e+zJv+oiVKKEWZPyqPAh4MQBI65gA== +"@testing-library/react@^13.1.1": + version "13.1.1" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-13.1.1.tgz#6c1635e25acca8ca5be8ee3b19ad1391681c5846" + integrity sha512-8mirlAa0OKaUvnqnZF6MdAh2tReYA2KtWVw1PKvaF5EcCZqgK5pl8iF+3uW90JdG5Ua2c2c2E2wtLdaug3dsVg== dependencies: "@babel/runtime" "^7.12.5" - "@testing-library/dom" "^8.0.0" + "@testing-library/dom" "^8.5.0" + "@types/react-dom" "^18.0.0" "@tootallnate/once@2": version "2.0.0" @@ -1934,10 +1935,17 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== -"@types/react@17.0.20": - version "17.0.20" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.20.tgz#a4284b184d47975c71658cd69e759b6bd37c3b8c" - integrity sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA== +"@types/react-dom@^18.0.0": + version "18.0.2" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.2.tgz#2d6b46557aa30257e87e67a6d952146d15979d79" + integrity sha512-UxeS+Wtj5bvLRREz9tIgsK4ntCuLDo0EcAcACgw3E+9wE8ePDr9uQpq53MfcyxyIS55xJ+0B6mDS8c4qkkHLBg== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^18.0.6": + version "18.0.6" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.6.tgz#30206c3830af6ce8639b91ace5868bc2d3d1d96c" + integrity sha512-bPqwzJRzKtfI0mVYr5R+1o9BOE8UEXefwc1LwcBtfnaAn6OoqMhLa/91VA8aeWfDPJt1kHvYKI8RHcQybZLHHA== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -1972,6 +1980,11 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + "@types/yargs-parser@*": version "20.2.1" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" @@ -5789,14 +5802,7 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -"react-18@npm:react@18": - version "18.0.0" - resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" - integrity sha512-x+VL6wbT4JRVPm7EGxXhZ8w8LTROaxPXOqhlGyVSrv0sB1jkyFGgXxJ8LVoPRLvPR6/CIZGFmfzqUa2NYeMr2A== - dependencies: - loose-envify "^1.1.0" - -"react-dom-18@npm:react-dom@18": +react-dom@^18.0.0: version "18.0.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.0.0.tgz#26b88534f8f1dbb80853e1eabe752f24100d8023" integrity sha512-XqX7uzmFo0pUceWFCt7Gff6IyIMzFUn7QMZrbrQfGxtaxXZIcGQzoNpRLE3fQLnS4XzLLPMZX2T9TRcSrasicw== @@ -5804,15 +5810,6 @@ randombytes@^2.1.0: loose-envify "^1.1.0" scheduler "^0.21.0" -react-dom@17.0.1: - version "17.0.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6" - integrity sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.1" - react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -5828,13 +5825,12 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== -react@17.0.1: - version "17.0.1" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127" - integrity sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w== +react@^18.0.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890" + integrity sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" read-pkg@^3.0.0: version "3.0.0" @@ -6104,14 +6100,6 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" -scheduler@^0.20.1: - version "0.20.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler@^0.21.0: version "0.21.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.21.0.tgz#6fd2532ff5a6d877b6edb12f00d8ab7e8f308820" @@ -6825,6 +6813,11 @@ use-subscription@1.5.1: dependencies: object-assign "^4.1.1" +use-sync-external-store@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz#3343c3fe7f7e404db70f8c687adf5c1652d34e82" + integrity sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"