From 1fb7098a1473fdcab07956430c388a9c736b8ad9 Mon Sep 17 00:00:00 2001 From: xobotyi Date: Sat, 15 May 2021 18:12:34 +0300 Subject: [PATCH] feat: fully refactor useStorageValue. --- src/index.ts | 1 - src/useLocalStorageValue.ts | 199 +--------- src/useSessionStorageValue.ts | 143 +------ src/useStorageValue.ts | 374 ++++++++++-------- src/util/const.ts | 2 - stories/Lifecycle/useIsMounted.story.mdx | 2 + stories/Lifecycle/useRerender.story.mdx | 2 + .../useLocalStorageValue.stories.tsx | 6 + .../useLocalStorageValue.story.mdx | 33 +- .../useSessionStorageValue.stories.tsx | 6 + .../useSessionStorageValue.story.mdx | 27 +- stories/SideEffects/useStorageValue.story.mdx | 11 - stories/State/useMediatedState.story.mdx | 2 + stories/State/useSafeState.story.mdx | 2 + stories/State/useToggle.story.mdx | 2 + tests/dom/useLocalStorageValue.test.ts | 4 +- tests/dom/useSessionStorageValue.test.ts | 14 + tests/dom/useStorageValue.test.ts | 325 +++++++++++---- tests/ssr/useLocalStorageValue.test.ts | 4 +- tests/ssr/useSessionStorageValue.test.ts | 14 + tests/ssr/useStorageValue.test.ts | 83 ++-- yarn.lock | 146 +++++-- 22 files changed, 763 insertions(+), 639 deletions(-) delete mode 100644 stories/SideEffects/useStorageValue.story.mdx create mode 100644 tests/dom/useSessionStorageValue.test.ts create mode 100644 tests/ssr/useSessionStorageValue.test.ts diff --git a/src/index.ts b/src/index.ts index 06e2f3014..75765dd3c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,4 +15,3 @@ export { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; export { useSyncedRef } from './useSyncedRef'; export { useLocalStorageValue } from './useLocalStorageValue'; export { useSessionStorageValue } from './useSessionStorageValue'; -export { useStorageValue } from './useStorageValue'; diff --git a/src/useLocalStorageValue.ts b/src/useLocalStorageValue.ts index c3f3a690e..52ad5be13 100644 --- a/src/useLocalStorageValue.ts +++ b/src/useLocalStorageValue.ts @@ -1,213 +1,44 @@ -/* eslint-disable @typescript-eslint/no-use-before-define */ +import { IHookReturn, IUseStorageValueOptions, useStorageValue } from './useStorageValue'; -import { useEffect, useMemo } from 'react'; -import { IUseStorageValueOptions, useStorageValue } from './useStorageValue'; -import { off, on } from './util/misc'; -import { isBrowser, noop } from './util/const'; -import { INextState } from './util/resolveHookState'; -import { useSyncedRef } from './useSyncedRef'; - -export type IUseLocalStorageValueOptions< - T, - Raw extends boolean | undefined = boolean | undefined, - InitializeWithValue extends boolean | undefined = boolean | undefined -> = IUseStorageValueOptions & { - /** - * Subscribe to window's `storage` event. - * - * @default true - */ - handleStorageEvent?: boolean; -}; - -// below typings monstrosity required to provide most accurate return type hint - -type IReturnState< - T, - D, - O, - S = O extends { raw: true } ? string : T, - N = D extends null | undefined ? null | S : S, - U = O extends { initializeWithStorageValue: false } ? undefined | N : N -> = U; - -type INewState = INextState< - S, - IReturnState ->; - -type IHookReturn = [IReturnState, (val: INewState) => void, () => void]; - -export function useLocalStorageValue( - key: string, - defaultValue?: null -): IHookReturn>; -export function useLocalStorageValue( - key: string, - defaultValue: null, - options: IUseLocalStorageValueOptions -): IHookReturn; -export function useLocalStorageValue( - key: string, - defaultValue: null, - options: IUseLocalStorageValueOptions -): IHookReturn; -export function useLocalStorageValue( - key: string, - defaultValue: null, - options: IUseLocalStorageValueOptions -): IHookReturn; export function useLocalStorageValue( key: string, - defaultValue: null, - options: IUseLocalStorageValueOptions -): IHookReturn; -export function useLocalStorageValue( - key: string, - defaultValue: null, - options: IUseLocalStorageValueOptions -): IHookReturn; + defaultValue?: null, + options?: IUseStorageValueOptions +): IHookReturn>; export function useLocalStorageValue( key: string, defaultValue: null, - options: IUseLocalStorageValueOptions + options: IUseStorageValueOptions ): IHookReturn; export function useLocalStorageValue( - key: string, - defaultValue: T -): IHookReturn>; - -export function useLocalStorageValue( - key: string, - defaultValue: null, - options: IUseLocalStorageValueOptions -): IHookReturn; -export function useLocalStorageValue( key: string, defaultValue: T, - options: IUseLocalStorageValueOptions -): IHookReturn; -export function useLocalStorageValue( - key: string, - defaultValue: T, - options: IUseLocalStorageValueOptions -): IHookReturn; -export function useLocalStorageValue( - key: string, - defaultValue: T, - options: IUseLocalStorageValueOptions -): IHookReturn; -export function useLocalStorageValue( - key: string, - defaultValue: T, - options: IUseLocalStorageValueOptions -): IHookReturn; -export function useLocalStorageValue( + options?: IUseStorageValueOptions +): IHookReturn>; +export function useLocalStorageValue( key: string, defaultValue: T, - options: IUseLocalStorageValueOptions + options: IUseStorageValueOptions ): IHookReturn; export function useLocalStorageValue( key: string, defaultValue?: T | null, - options?: IUseLocalStorageValueOptions + options?: IUseStorageValueOptions ): IHookReturn; /** - * Manages a single LocalStorage key. + * Manages a single localStorage key. * - * @param key LocalStorage key to manage - * @param defaultValue Default value to return in case key not presented in LocalStorage + * @param key Storage key to manage + * @param defaultValue Default value to yield in case the key is not in storage * @param options */ export function useLocalStorageValue( key: string, defaultValue: T | null = null, - options: IUseLocalStorageValueOptions = {} + options: IUseStorageValueOptions = {} ): IHookReturn { - const { handleStorageEvent = true, ...storageOptions } = options; - const [value, setValue, removeValue, fetchValue] = useStorageValue( - localStorage, - key, - defaultValue, - storageOptions - ); - - const keyRef = useSyncedRef(key); - - useEffect(() => { - if (!isBrowser || !handleStorageEvent) return; - - const storageHandler = (ev: StorageEvent) => { - if (ev.storageArea !== localStorage) return; - if (ev.key !== keyRef.current) return; - - fetchValue(); - }; - - on(window, 'storage', storageHandler, { passive: true }); - - // eslint-disable-next-line consistent-return - return () => { - off(window, 'storage', storageHandler); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [handleStorageEvent]); - - // keep actual key in hooks registry - useEffect(() => { - if (!usedStorageKeys.has(key)) { - usedStorageKeys.set(key, []); - } - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const fetchers = usedStorageKeys.get(key)!; - - fetchers.push(fetchValue); - - return () => { - const idx = fetchers.indexOf(fetchValue); - if (idx !== -1) { - fetchers.splice(idx, 1); - } - - if (!fetchers.length) usedStorageKeys.delete(key); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [key]); - - // wrapped methods call others hooks `fetchValue` to synchronise state - const wrappedMethods = useMemo( - () => ({ - setValue: ((val) => { - setValue(val); - - usedStorageKeys.get(keyRef.current)?.forEach((fetcher) => { - if (fetcher === fetchValue) return; - - fetcher(); - }); - }) as typeof setValue, - removeValue: (() => { - removeValue(); - - usedStorageKeys.get(keyRef.current)?.forEach((fetcher) => { - if (fetcher === fetchValue) return; - - fetcher(); - }); - }) as typeof removeValue, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - // SSR version should literally do nothing to avoid requests to local storage - if (!isBrowser) return [undefined, noop, noop]; - - return [value, wrappedMethods.setValue, wrappedMethods.removeValue]; + return useStorageValue(localStorage, key, defaultValue, options); } - -const usedStorageKeys = new Map void)[]>(); diff --git a/src/useSessionStorageValue.ts b/src/useSessionStorageValue.ts index 26b67e001..bd103c771 100644 --- a/src/useSessionStorageValue.ts +++ b/src/useSessionStorageValue.ts @@ -1,157 +1,44 @@ -import { useEffect } from 'react'; -import { IUseStorageValueOptions, useStorageValue } from './useStorageValue'; -import { off, on } from './util/misc'; -import { isBrowser } from './util/const'; -import { INextState } from './util/resolveHookState'; -import { useSyncedRef } from './useSyncedRef'; +import { IHookReturn, IUseStorageValueOptions, useStorageValue } from './useStorageValue'; -export type IUseSessionStorageValueOptions< - T, - Raw extends boolean | undefined = boolean | undefined, - InitializeWithValue extends boolean | undefined = boolean | undefined -> = IUseStorageValueOptions & { - /** - * Subscribe to window's `storage` event. - * - * @default true - */ - handleStorageEvent?: boolean; -}; - -// below typings monstrosity required to provide most accurate return type hint - -type IReturnState< - T, - D, - O, - S = O extends { raw: true } ? string : T, - N = D extends null | undefined ? null | S : S, - U = O extends { initializeWithStorageValue: false } ? undefined | N : N -> = U; - -type INewState = INextState< - S, - IReturnState ->; - -type IHookReturn = [IReturnState, (val: INewState) => void, () => void]; - -export function useSessionStorageValue( - key: string, - defaultValue?: null -): IHookReturn>; export function useSessionStorageValue( key: string, - defaultValue: null, - options: IUseSessionStorageValueOptions -): IHookReturn; + defaultValue?: null, + options?: IUseStorageValueOptions +): IHookReturn>; export function useSessionStorageValue( key: string, defaultValue: null, - options: IUseSessionStorageValueOptions -): IHookReturn; -export function useSessionStorageValue( - key: string, - defaultValue: null, - options: IUseSessionStorageValueOptions -): IHookReturn; -export function useSessionStorageValue( - key: string, - defaultValue: null, - options: IUseSessionStorageValueOptions -): IHookReturn; -export function useSessionStorageValue( - key: string, - defaultValue: null, - options: IUseSessionStorageValueOptions -): IHookReturn; -export function useSessionStorageValue( - key: string, - defaultValue: null, - options: IUseSessionStorageValueOptions + options: IUseStorageValueOptions ): IHookReturn; export function useSessionStorageValue( - key: string, - defaultValue: T -): IHookReturn>; - -export function useSessionStorageValue( - key: string, - defaultValue: null, - options: IUseSessionStorageValueOptions -): IHookReturn; -export function useSessionStorageValue( - key: string, - defaultValue: T, - options: IUseSessionStorageValueOptions -): IHookReturn; -export function useSessionStorageValue( - key: string, - defaultValue: T, - options: IUseSessionStorageValueOptions -): IHookReturn; -export function useSessionStorageValue( key: string, defaultValue: T, - options: IUseSessionStorageValueOptions -): IHookReturn; -export function useSessionStorageValue( - key: string, - defaultValue: T, - options: IUseSessionStorageValueOptions -): IHookReturn; -export function useSessionStorageValue( + options?: IUseStorageValueOptions +): IHookReturn>; +export function useSessionStorageValue( key: string, defaultValue: T, - options: IUseSessionStorageValueOptions + options: IUseStorageValueOptions ): IHookReturn; export function useSessionStorageValue( key: string, defaultValue?: T | null, - options?: IUseSessionStorageValueOptions + options?: IUseStorageValueOptions ): IHookReturn; /** - * Manages a single SessionStorage key. + * Manages a single sessionStorage key. * - * @param key SessionStorage key to manage - * @param defaultValue Default value to return in case key not presented in SessionStorage + * @param key Storage key to manage + * @param defaultValue Default value to yield in case the key is not in storage * @param options */ export function useSessionStorageValue( key: string, defaultValue: T | null = null, - options: IUseSessionStorageValueOptions = {} + options: IUseStorageValueOptions = {} ): IHookReturn { - const { handleStorageEvent = true, ...storageOptions } = options; - const [value, setValue, removeValue, fetchValue] = useStorageValue( - sessionStorage, - key, - defaultValue, - storageOptions - ); - const keyRef = useSyncedRef(key); - - useEffect(() => { - if (!isBrowser || !handleStorageEvent) return; - - const storageHandler = (ev: StorageEvent) => { - if (ev.storageArea !== sessionStorage) return; - if (ev.key !== keyRef.current) return; - - fetchValue(); - }; - - on(window, 'storage', storageHandler, { passive: true }); - - // eslint-disable-next-line consistent-return - return () => { - off(window, 'storage', storageHandler); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [handleStorageEvent]); - - return [value, setValue, removeValue]; + return useStorageValue(localStorage, key, defaultValue, options); } diff --git a/src/useStorageValue.ts b/src/useStorageValue.ts index 0aa2537a1..e651e4976 100644 --- a/src/useStorageValue.ts +++ b/src/useStorageValue.ts @@ -1,4 +1,5 @@ -import { useCallback } from 'react'; +/* eslint-disable @typescript-eslint/no-use-before-define */ +import { useCallback, useEffect } from 'react'; import { useSafeState } from './useSafeState'; import { useConditionalEffect } from './useConditionalEffect'; import { INextState, resolveHookState } from './util/resolveHookState'; @@ -8,276 +9,319 @@ import { useSyncedRef } from './useSyncedRef'; import { isBrowser } from './util/const'; import { useFirstMountState } from './useFirstMountState'; import { usePrevious } from './usePrevious'; - -export type IUseStorageValueAdapter = { - getItem(key: string): string | null; - - setItem(key: string, value: string): void; - - removeItem(key: string): void; -}; +import { off, on } from './util/misc'; export type IUseStorageValueOptions< - T, - Raw extends boolean | undefined = boolean | undefined, InitializeWithValue extends boolean | undefined = boolean | undefined > = { /** * Whether to store default value to store. + * + * @default false */ storeDefaultValue?: boolean; /** - * Serializer to use in non-raw mode. + * Disable synchronisation with other hook instances managing same key on same page. * - * @default JSON.stringify + * @default false */ - serializer?: (value: T) => string; + isolated?: boolean; /** - * Deserializer to use in non-raw mode. + * Subscribe to window's `storage` event. * - * @default JSON.parse + * @default true */ - deserializer?: (str: string) => T; -} & (Raw extends undefined + handleStorageEvent?: boolean; +} & (InitializeWithValue extends undefined ? { /** - * Whether to use raw, string values. + * Whether to perform value fetch from storage or initialize with `undefined` state. * - * In case raw is set to true, you will be restricted to use string values only. + * Default to false during SSR * - * @default false + * @default true */ - raw?: Raw; + initializeWithStorageValue?: InitializeWithValue; } : { - raw: Raw; - }) & - (InitializeWithValue extends undefined - ? { - /** - * Whether to perform value fetch from storage or initialize with `undefined` state. - * - * @default true - */ - initializeWithStorageValue?: InitializeWithValue; - } - : { - initializeWithStorageValue: InitializeWithValue; - }); - -// below typings monstrosity required to provide most accurate return type hint + initializeWithStorageValue: InitializeWithValue; + }); -type IReturnState< +export type IReturnState< T, D, O, - S = O extends { raw: true } ? string : T, - N = D extends null | undefined ? null | S : S, + N = D extends null | undefined ? null | T : T, U = O extends { initializeWithStorageValue: false } ? undefined | N : N > = U; -type INewState = INextState< - S, - IReturnState ->; - -type IHookReturn = [ +export type IHookReturn = [ IReturnState, - (val: INewState) => void, + (val: INextState>) => void, () => void, () => void ]; export function useStorageValue( - adapter: IUseStorageValueAdapter, - key: string, - defaultValue?: null -): IHookReturn>; -export function useStorageValue( - adapter: IUseStorageValueAdapter, - key: string, - defaultValue: null, - options: IUseStorageValueOptions -): IHookReturn; -export function useStorageValue( - adapter: IUseStorageValueAdapter, - key: string, - defaultValue: null, - options: IUseStorageValueOptions -): IHookReturn; -export function useStorageValue( - adapter: IUseStorageValueAdapter, - key: string, - defaultValue: null, - options: IUseStorageValueOptions -): IHookReturn; -export function useStorageValue( - adapter: IUseStorageValueAdapter, + storage: Storage, key: string, - defaultValue: null, - options: IUseStorageValueOptions -): IHookReturn; -export function useStorageValue( - adapter: IUseStorageValueAdapter, - key: string, - defaultValue: null, - options: IUseStorageValueOptions -): IHookReturn; + defaultValue?: null, + options?: IUseStorageValueOptions +): IHookReturn>; export function useStorageValue( - adapter: IUseStorageValueAdapter, + storage: Storage, key: string, defaultValue: null, - options: IUseStorageValueOptions + options: IUseStorageValueOptions ): IHookReturn; export function useStorageValue( - adapter: IUseStorageValueAdapter, - key: string, - defaultValue: T -): IHookReturn>; -export function useStorageValue( - adapter: IUseStorageValueAdapter, + storage: Storage, key: string, defaultValue: T, - options: IUseStorageValueOptions -): IHookReturn; + options?: IUseStorageValueOptions +): IHookReturn>; export function useStorageValue( - adapter: IUseStorageValueAdapter, + storage: Storage, key: string, defaultValue: T, - options: IUseStorageValueOptions -): IHookReturn; -export function useStorageValue( - adapter: IUseStorageValueAdapter, - key: string, - defaultValue: T, - options: IUseStorageValueOptions -): IHookReturn; -export function useStorageValue( - adapter: IUseStorageValueAdapter, - key: string, - defaultValue: T, - options: IUseStorageValueOptions -): IHookReturn; -export function useStorageValue( - adapter: IUseStorageValueAdapter, - key: string, - defaultValue: T, - options: IUseStorageValueOptions -): IHookReturn; -export function useStorageValue( - adapter: IUseStorageValueAdapter, - key: string, - defaultValue: T, - options: IUseStorageValueOptions + options: IUseStorageValueOptions ): IHookReturn; export function useStorageValue( - adapter: IUseStorageValueAdapter, + storage: Storage, key: string, defaultValue?: T | null, - options?: IUseStorageValueOptions + options?: IUseStorageValueOptions ): IHookReturn; /** * Manages a single storage key. * - * @param adapter storage key to manage - * @param key storage key to manage - * @param defaultValue Default value to return in case key not presented in storage + * @param storage Storage instance that will be managed + * @param key Storage key to manage + * @param defaultValue Default value to yield in case the key is not in storage * @param options */ export function useStorageValue( - adapter: IUseStorageValueAdapter, + storage: Storage, key: string, defaultValue: T | null = null, - options: IUseStorageValueOptions = {} + options: IUseStorageValueOptions = {} ): IHookReturn { - const { - deserializer = JSON.parse, - serializer = JSON.stringify, - raw, - initializeWithStorageValue = true, - storeDefaultValue, - } = options; + const { isolated } = options; + let { initializeWithStorageValue = true, handleStorageEvent = true, storeDefaultValue } = options; - const methods = useSyncedRef({ - getVal: () => { - const val = adapter.getItem(key); + // avoid fetching data from storage during SSR + if (!isBrowser) { + storeDefaultValue = false; + initializeWithStorageValue = false; + handleStorageEvent = false; + } - if (val === null) return defaultValue; + // needed to provide stable API + const methods = useSyncedRef({ + fetchVal: () => parse(storage.getItem(key), defaultValue), + storeVal: (val: T) => { + const stringified = stringify(val); - return raw ? val : deserializer(val as string); - }, - setVal: (val: T | string) => { - if (raw) { - if (typeof val !== 'string') { - throw new TypeError( - 'value has to be a string, define serializer or cast it to string manually' - ); - } - } else { - val = serializer(val as T); + if (stringified) { + storage.setItem(key, stringified); + return true; } - adapter.setItem(key, val); - }, - fetchVal: () => { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - setState(methods.current.getVal()); + return false; }, removeVal: () => { - adapter.removeItem(key); - // eslint-disable-next-line @typescript-eslint/no-use-before-define - setState(defaultValue); + storage.removeItem(key); + }, + setVal: (val: string | null) => { + setState(parse(val, defaultValue) as T); + }, + fetchState: () => { + const newVal = methods.current.fetchVal() as T; + setState(newVal); + + return newVal !== stateRef.current ? newVal : null; + }, + setState: (nextState: T | null) => { + setState(nextState === null ? defaultValue : nextState); }, }); const isFirstMount = useFirstMountState(); - const [state, setState] = useSafeState( - initializeWithStorageValue && isBrowser && isFirstMount ? methods.current.getVal() : undefined + const [state, setState] = useSafeState( + initializeWithStorageValue && isFirstMount ? (methods.current.fetchVal() as T) : undefined ); const prevState = usePrevious(state); const stateRef = useSyncedRef(state); + const keyRef = useSyncedRef(key); + const isolatedRef = useSyncedRef(isolated); - // fetch value on mount for the case `initializeWithStorageValue` is false + // fetch value on mount for the case `initializeWithStorageValue` is false, + // effects are not invoked during SSR, so there is no need to check isBrowser here useMountEffect(() => { - if (!initializeWithStorageValue || !isBrowser) { - methods.current.fetchVal(); + if (!initializeWithStorageValue) { + methods.current.fetchState(); } }); // store default value if it is not null and options configured to store default value useConditionalEffect(() => { - methods.current.setVal(defaultValue as T); + methods.current.storeVal(defaultValue as T); }, [prevState !== state, state === defaultValue && defaultValue !== null && storeDefaultValue]); // refetch value when key changed useUpdateEffect(() => { - methods.current.fetchVal(); + methods.current.fetchState(); }, [key]); + // register hook for same-page synchronisation + useEffect(() => {}, [key]); + + // subscribe hook for storage events + useEffect(() => { + if (!handleStorageEvent) return; + + const storageHandler = (ev: StorageEvent) => { + if (ev.storageArea !== storage) return; + if (ev.key !== keyRef.current) return; + + methods.current.setVal(ev.newValue); + }; + + on(window, 'storage', storageHandler, { passive: true }); + + // eslint-disable-next-line consistent-return + return () => { + off(window, 'storage', storageHandler); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [handleStorageEvent]); + + useEffect(() => { + if (isolated) return; + + let storageKeys = storageKeysUsed.get(storage); + + if (!storageKeys) { + storageKeys = new Map(); + storageKeysUsed.set(storage, storageKeys); + } + + let keySetters = storageKeys.get(key); + + if (!keySetters) { + keySetters = new Set(); + storageKeys.set(key, keySetters); + } + + const mSetState = methods.current.setState; + keySetters.add(mSetState); + + // eslint-disable-next-line consistent-return + return () => { + keySetters?.delete(mSetState); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isolated, key]); + return [ state, useCallback( (newState) => { - const s = resolveHookState(newState, stateRef.current as T); + if (!isBrowser) return; + + const s = resolveHookState(newState, stateRef.current); + + if (methods.current.storeVal(s)) { + methods.current.setState(s); + + if (!isolatedRef.current) { + storageKeysUsed + .get(storage) + ?.get(keyRef.current) + ?.forEach((setter) => { + if (setter === methods.current.setState) return; - methods.current.setVal(s); - setState(s); + setter(s); + }); + } + } }, // eslint-disable-next-line react-hooks/exhaustive-deps [] ), useCallback(() => { + if (!isBrowser) return; + methods.current.removeVal(); + methods.current.setState(null); + + if (!isolatedRef.current) { + // update all other hooks state + storageKeysUsed + .get(storage) + ?.get(keyRef.current) + ?.forEach((setter) => { + if (setter === methods.current.setState) return; + + setter(null); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []), useCallback(() => { - methods.current.fetchVal(); + if (!isBrowser) return; + + const newVal = methods.current.fetchState(); + if (newVal !== null) { + if (!isolatedRef.current) { + // update all other hooks state + storageKeysUsed + .get(storage) + ?.get(keyRef.current) + ?.forEach((setter) => { + if (setter === methods.current.setState) return; + + setter(newVal); + }); + } + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []), ]; } + +const storageKeysUsed = new Map>>(); + +const stringify = (data: unknown): string | null => { + if (data === null) { + console.warn( + `'null' is not a valid data for useStorageValue hook, this operation will take no effect` + ); + return null; + } + + try { + return JSON.stringify(data); + } catch (e) /* istanbul ignore next */ { + // i have absolutely no idea how to cover this, since modern JSON.stringify does not throw on + // cyclic references anymore + console.warn(e); + return null; + } +}; +const parse = (str: string | null, fallback: unknown): unknown => { + if (str === null) return fallback; + + try { + return JSON.parse(str); + } catch (e) { + console.warn(e); + return fallback; + } +}; diff --git a/src/util/const.ts b/src/util/const.ts index 2b8bffcfe..7af5ce009 100644 --- a/src/util/const.ts +++ b/src/util/const.ts @@ -1,7 +1,5 @@ export const noop = (): void => {}; -export const bypass = (v: T): T => v; - export const isBrowser = typeof window !== 'undefined' && typeof navigator !== 'undefined' && diff --git a/stories/Lifecycle/useIsMounted.story.mdx b/stories/Lifecycle/useIsMounted.story.mdx index f4517884a..9db2e09da 100644 --- a/stories/Lifecycle/useIsMounted.story.mdx +++ b/stories/Lifecycle/useIsMounted.story.mdx @@ -8,6 +8,8 @@ import { Example } from './useIsMounted.stories'; Returns function that yields current mount state. This hook is handy for the cases when you have to detect component mount state within async effects. +> **_This hook provides stable API, meaning returned methods does not change between renders_** + #### Example diff --git a/stories/Lifecycle/useRerender.story.mdx b/stories/Lifecycle/useRerender.story.mdx index bf03f3806..77eec9e8c 100644 --- a/stories/Lifecycle/useRerender.story.mdx +++ b/stories/Lifecycle/useRerender.story.mdx @@ -7,6 +7,8 @@ import { Example } from './useRerender.stories'; Return callback function that re-renders component. +> **_This hook provides stable API, meaning returned methods does not change between renders_** + #### Example diff --git a/stories/SideEffects/useLocalStorageValue.stories.tsx b/stories/SideEffects/useLocalStorageValue.stories.tsx index 3217accda..8f23a5304 100644 --- a/stories/SideEffects/useLocalStorageValue.stories.tsx +++ b/stories/SideEffects/useLocalStorageValue.stories.tsx @@ -14,15 +14,21 @@ interface IExampleProps { * Subscribe to window's `storage` event. */ handleStorageEvent: boolean; + /** + * Isolate hook from others on page - it will not receive updates from other hooks managing same key. + */ + isolated: boolean; } export const Example: React.FC = ({ key = 'react-hookz-ls-test', defaultValue = '@react-hookz is awesome', handleStorageEvent = true, + isolated = false, }) => { const [value, setValue, removeValue] = useLocalStorageValue(key, defaultValue, { handleStorageEvent, + isolated, }); return ( diff --git a/stories/SideEffects/useLocalStorageValue.story.mdx b/stories/SideEffects/useLocalStorageValue.story.mdx index 6e5ca4da9..287a6056f 100644 --- a/stories/SideEffects/useLocalStorageValue.story.mdx +++ b/stories/SideEffects/useLocalStorageValue.story.mdx @@ -7,7 +7,15 @@ import {ArgsTable, Canvas, Meta, Story} from "@storybook/addon-docs/blocks"; Manages a single LocalStorage key. -> By default, tracks window's `storage` event and automatically updates state. +> **_This hook provides stable API, meaning returned methods does not change between renders_** + +> This hook uses `useSafeState` underneath, so it is safe to use its `setState` in async hooks. + +> Does not allow usage of `null` value, since JSON allows serializing `null` values - it would be +impossible to separate null value fom 'no such value' API result which is also `null`. + +> By default, tracks window's `storage` event and automatically synchronizes all hooks on the page +managing same key. > While using SSR, to avoid hydration mismatch, consider setting `initializeWithStorageValue` option to `false`, this will yield `undefined` state on first render and defer value fetch to effects @@ -17,9 +25,9 @@ execution stage. - +
It also synchronised between hooks on same page - +
@@ -31,30 +39,27 @@ execution stage. function useLocalStorageValue( key: string, defaultValue: T | null = null, - options: IUseLocalStorageValueOptions = {} + options: IUseStorageValueOptions = {} ): IHookReturn ``` #### Arguments - **key** _`string`_ - LocalStorage key to manage. -- **defaultValue** _`T | null`_ _(default: null)_ - Default value to return in case key not +- **defaultValue** _`T | null`_ _(default: null)_ - Default value to return in case key not presented in LocalStorage. - **options** _`object`_ - Hook options: + - **isolated** _`boolean`_ _(default: false)_ - Disable synchronisation with other hook instances + managing same key on same page. - **handleStorageEvent** _`boolean`_ _(default: true)_ - Subscribe to window's `storage` event. - - **raw** _`boolean`_ _(default: false)_ - do not serialize and deserialize values, it restricts - value type to string only. - **storeDefaultValue** _`boolean`_ _(default: false)_ - store default value. - - **initializeWithStorageValue** _`boolean`_ _(default: true)_ - fetch storage value on first - render. If set to `false` will make hook to yield `undefined` state on first render and defer - value fetch to effects execution stage. - - **serializer** _`(value: T) => string`_ _(default: JSON.stringify)_ - Serializer to use in non-raw - mode. - - **deserializer** _`(str: string) => T`_ _(default: JSON.parse)_ - Deserializer to use in non-raw - mode. + - **initializeWithStorageValue** _`boolean`_ _(default: true)_ - fetch storage value on first + render. If set to `false` will make hook to yield `undefined` state on first render and defer + value fetch till effects execution stage. #### Return 0. **state** - LocalStorage item value or default value in case of item absence. 1. **setValue** - Method to set new item value. 2. **removeValue** - Method to remove item from storage. +3. **fetchValue** - Method to pull value from localStorage. diff --git a/stories/SideEffects/useSessionStorageValue.stories.tsx b/stories/SideEffects/useSessionStorageValue.stories.tsx index 420a1b516..8d10d213d 100644 --- a/stories/SideEffects/useSessionStorageValue.stories.tsx +++ b/stories/SideEffects/useSessionStorageValue.stories.tsx @@ -14,15 +14,21 @@ interface IExampleProps { * Subscribe to window's `storage` event. */ handleStorageEvent: boolean; + /** + * Isolate hook from others on page - it will not receive updates from other hooks managing same key. + */ + isolated: boolean; } export const Example: React.FC = ({ key = 'react-hookz-ss-test', defaultValue = '@react-hookz is awesome', handleStorageEvent = true, + isolated = false, }) => { const [value, setValue, removeValue] = useSessionStorageValue(key, defaultValue, { handleStorageEvent, + isolated, }); return ( diff --git a/stories/SideEffects/useSessionStorageValue.story.mdx b/stories/SideEffects/useSessionStorageValue.story.mdx index 006c05eac..109b0e32d 100644 --- a/stories/SideEffects/useSessionStorageValue.story.mdx +++ b/stories/SideEffects/useSessionStorageValue.story.mdx @@ -7,7 +7,15 @@ import {ArgsTable, Canvas, Meta, Story} from "@storybook/addon-docs/blocks"; Manages a single SessionStorage key. -> By default, tracks window's `storage` event and automatically updates state. +> **_This hook provides stable API, meaning returned methods does not change between renders_** + +> This hook uses `useSafeState` underneath, so it is safe to use its `setState` in async hooks. + +> Does not allow usage of `null` value, since JSON allows serializing `null` values - it would be +impossible to separate null value fom 'no such value' API result which is also `null`. + +> By default, tracks window's `storage` event and automatically synchronizes all hooks on the page +managing same key. > While using SSR, to avoid hydration mismatch, consider setting `initializeWithStorageValue` option to `false`, this will yield `undefined` state on first render and defer value fetch to effects @@ -17,6 +25,10 @@ execution stage. +
+ It also synchronised between hooks on same page +
+
@@ -27,7 +39,7 @@ execution stage. function useSessionStorageValue( key: string, defaultValue: T | null = null, - options: IUseSessionStorageValueOptions = {} + options: IUseStorageValueOptions = {} ): IHookReturn ``` @@ -37,20 +49,17 @@ function useSessionStorageValue( - **defaultValue** _`T | null`_ _(default: null)_ - Default value to return in case key not presented in SessionStorage. - **options** _`object`_ - Hook options: + - **isolated** _`boolean`_ _(default: false)_ - Disable synchronisation with other hook instances + managing same key on same page. - **handleStorageEvent** _`boolean`_ _(default: true)_ - Subscribe to window's `storage` event. - - **raw** _`boolean`_ _(default: false)_ - do not serialize and deserialize values, it restricts - value type to string only. - **storeDefaultValue** _`boolean`_ _(default: false)_ - store default value. - **initializeWithStorageValue** _`boolean`_ _(default: true)_ - fetch storage value on first render. If set to `false` will make hook to yield `undefined` state on first render and defer - value fetch to effects execution stage. - - **serializer** _`(value: T) => string`_ _(default: JSON.stringify)_ - Serializer to use in non-raw - mode. - - **deserializer** _`(str: string) => T`_ _(default: JSON.parse)_ - Deserializer to use in non-raw - mode. + value fetch till effects execution stage. #### Return 0. **state** - SessionStorage item value or default value in case of item absence. 1. **setValue** - Method to set new item value. 2. **removeValue** - Method to remove item from storage. +3. **fetchValue** - Method to pull value from sessionStorage. diff --git a/stories/SideEffects/useStorageValue.story.mdx b/stories/SideEffects/useStorageValue.story.mdx deleted file mode 100644 index a25bb047f..000000000 --- a/stories/SideEffects/useStorageValue.story.mdx +++ /dev/null @@ -1,11 +0,0 @@ -import {Meta} from "@storybook/addon-docs/blocks"; - - - -# useStorageValue - -Manages a single storage key. General-purpose hook for string key-value storages, that used -in [`useLocalStorageValue`](/docs/side-effects-uselocalstoragevalue--example) and -[`useSessionStorageValue`](/docs/side-effects-usesessionstoragevalue--example). - -> This hook requires storage to be synchronous, string-value storage. diff --git a/stories/State/useMediatedState.story.mdx b/stories/State/useMediatedState.story.mdx index 6329029ab..109ac8d3c 100644 --- a/stories/State/useMediatedState.story.mdx +++ b/stories/State/useMediatedState.story.mdx @@ -11,6 +11,8 @@ Like `useState`, but every value set is passed through mediator function. > `setState` returned by this hook is stable between renders. +> **_This hook provides stable API, meaning returned functions does not change between renders_** + #### Example diff --git a/stories/State/useSafeState.story.mdx b/stories/State/useSafeState.story.mdx index 6a7adfbbf..14561fede 100644 --- a/stories/State/useSafeState.story.mdx +++ b/stories/State/useSafeState.story.mdx @@ -12,6 +12,8 @@ warning happens, and you have to track component mount state manually. `useSafeState` covers your back - it tracks component mount state and does not perform `setState` action if component is unmounted, otherwise it is the same hook as common `useState`. +> **_This hook provides stable API, meaning returned functions does not change between renders_** + #### Example Sadly we can't provide an example since this documentation built in `production` mode and warning diff --git a/stories/State/useToggle.story.mdx b/stories/State/useToggle.story.mdx index 2e43a86f6..da99f81b5 100644 --- a/stories/State/useToggle.story.mdx +++ b/stories/State/useToggle.story.mdx @@ -7,6 +7,8 @@ import { Example } from './useToggle.stories'; Like `useState`, but can only become `true` or `false`. +> **_This hook provides stable API, meaning returned functions does not change between renders_** + #### Example diff --git a/tests/dom/useLocalStorageValue.test.ts b/tests/dom/useLocalStorageValue.test.ts index 6d47d565d..aa1ad14d8 100644 --- a/tests/dom/useLocalStorageValue.test.ts +++ b/tests/dom/useLocalStorageValue.test.ts @@ -7,6 +7,8 @@ describe('useLocalStorageValue', () => { }); it('should render', () => { - renderHook(() => useLocalStorageValue('foo')); + renderHook(() => { + useLocalStorageValue('foo'); + }); }); }); diff --git a/tests/dom/useSessionStorageValue.test.ts b/tests/dom/useSessionStorageValue.test.ts new file mode 100644 index 000000000..e69673d34 --- /dev/null +++ b/tests/dom/useSessionStorageValue.test.ts @@ -0,0 +1,14 @@ +import { renderHook } from '@testing-library/react-hooks/dom'; +import { useSessionStorageValue } from '../../src'; + +describe('useSessionStorageValue', () => { + it('should be defined', () => { + expect(useSessionStorageValue).toBeDefined(); + }); + + it('should render', () => { + renderHook(() => { + useSessionStorageValue('foo'); + }); + }); +}); diff --git a/tests/dom/useStorageValue.test.ts b/tests/dom/useStorageValue.test.ts index 0cf27b81f..875247a26 100644 --- a/tests/dom/useStorageValue.test.ts +++ b/tests/dom/useStorageValue.test.ts @@ -1,21 +1,24 @@ import { act, renderHook } from '@testing-library/react-hooks/dom'; -import { useStorageValue } from '../../src'; +import { useStorageValue } from '../../src/useStorageValue'; +import Mocked = jest.Mocked; describe('useStorageValue', () => { it('should be defined', () => { expect(useStorageValue).toBeDefined(); }); - const adapter = { - getItem: jest.fn(), - setItem: jest.fn(), - removeItem: jest.fn(), - }; + const adapter = ({ + getItem: jest.fn(() => null), + + setItem: jest.fn(() => {}), + + removeItem: jest.fn(() => {}), + } as unknown) as Mocked; beforeEach(() => { - adapter.getItem.mockReset(); - adapter.setItem.mockReset(); - adapter.removeItem.mockReset(); + adapter.getItem.mockClear().mockImplementation(() => null); + adapter.setItem.mockClear(); + adapter.removeItem.mockClear(); }); it('should render', () => { @@ -33,7 +36,7 @@ describe('useStorageValue', () => { expect(adapter.getItem).toHaveBeenCalledTimes(1); }); - it('should pass value through JSON.parse during fetch in on-raw mode', () => { + it('should pass value through JSON.parse during fetch', () => { const JSONParseSpy = jest.spyOn(JSON, 'parse'); adapter.getItem.mockImplementationOnce((key) => `"${key}"`); const { result } = renderHook(() => useStorageValue(adapter, 'foo')); @@ -42,32 +45,22 @@ describe('useStorageValue', () => { JSONParseSpy.mockRestore(); }); - it('should not pass value through JSON.parse during fetch in raw mode', () => { - const JSONParseSpy = jest.spyOn(JSON, 'parse'); - adapter.getItem.mockImplementationOnce((key) => `"${key}"`); - const { result } = renderHook(() => useStorageValue(adapter, 'foo', null, { raw: true })); - expect(result.current[0]).toBe('"foo"'); - expect(JSONParseSpy).not.toHaveBeenCalled(); - JSONParseSpy.mockRestore(); - }); - - it('should pass value through provided deserializer during fetch in on-raw mode', () => { - adapter.getItem.mockImplementationOnce((key) => `"${key}"`); - const deserializerSpy = jest.fn((val) => JSON.parse(val)); - const { result } = renderHook(() => - useStorageValue(adapter, 'foo', null, { deserializer: deserializerSpy }) - ); - expect(result.current[0]).toBe('foo'); - expect(deserializerSpy).toHaveBeenCalledWith('"foo"'); - }); - - it('should yield default value in case of storage returned null during fetch', () => { + it('should yield default value in case storage returned null during fetch', () => { adapter.getItem.mockImplementationOnce(() => null); const { result } = renderHook(() => useStorageValue(adapter, 'foo', 'defaultValue')); expect(result.current[0]).toBe('defaultValue'); expect(adapter.getItem).toHaveBeenCalledWith('foo'); }); + it('should yield default value and console.warn in case storage returned corrupted JSON', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementationOnce(() => {}); + adapter.getItem.mockImplementationOnce(() => 'corrupted JSON'); + const { result } = renderHook(() => useStorageValue(adapter, 'foo', 'defaultValue')); + expect(result.current[0]).toBe('defaultValue'); + expect(warnSpy.mock.calls[0][0]).toBeInstanceOf(SyntaxError); + warnSpy.mockRestore(); + }); + it('should not fetch value on first render in case `initializeWithStorageValue` options is set to false', () => { adapter.getItem.mockImplementationOnce(() => '"bar"'); const { result } = renderHook(() => @@ -97,7 +90,7 @@ describe('useStorageValue', () => { expect(spySetter).toHaveBeenCalledWith('bar'); }); - it('should call JSON.stringify on setState call in non-raw mode', () => { + it('should call JSON.stringify on setState call', () => { const JSONStringifySpy = jest.spyOn(JSON, 'stringify'); adapter.getItem.mockImplementationOnce(() => null); const { result } = renderHook(() => useStorageValue(adapter, 'foo', null)); @@ -111,35 +104,25 @@ describe('useStorageValue', () => { JSONStringifySpy.mockRestore(); }); - it('should call provided serializer on setState call in non-raw mode', () => { - const serializerSpy = jest.fn((v) => JSON.stringify(v)); - adapter.getItem.mockImplementationOnce(() => null); - const { result } = renderHook(() => - useStorageValue(adapter, 'foo', null, { serializer: serializerSpy }) - ); + it('should not store null or data that cannot be processed by JSON serializer and emit console.warn', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementationOnce(() => {}); + adapter.getItem.mockImplementationOnce(() => '"bar"'); + const { result } = renderHook(() => useStorageValue(adapter, 'foo', 'default value')); - expect(result.current[0]).toBe(null); + const invalidData: { a?: unknown } = {}; + invalidData.a = { b: invalidData }; + + expect(result.current[0]).toBe('bar'); act(() => { - result.current[1]('bar'); + // @ts-expect-error testing inappropriate use + result.current[1](null); }); expect(result.current[0]).toBe('bar'); - expect(serializerSpy).toHaveBeenCalledWith('bar'); - }); - - it('should throw in case non-string value been set in raw mode', () => { - adapter.getItem.mockImplementationOnce(() => null); - const { result } = renderHook(() => - useStorageValue(adapter, 'foo', null, { raw: true }) + expect(warnSpy).toHaveBeenCalledWith( + `'null' is not a valid data for useStorageValue hook, this operation will take no effect` ); - expect(() => { - act(() => { - // @ts-expect-error testing inappropriate usage - result.current[1](123); - }); - }).toThrow( - new TypeError('value has to be a string, define serializer or cast it to string manually') - ); + warnSpy.mockRestore(); }); it('should call storage`s removeItem on item remove', () => { @@ -153,10 +136,8 @@ describe('useStorageValue', () => { }); it('should set state to default value on item remove', () => { - adapter.getItem.mockImplementationOnce(() => 'bar'); - const { result } = renderHook(() => - useStorageValue(adapter, 'foo', 'default value', { raw: true }) - ); + adapter.getItem.mockImplementationOnce(() => '"bar"'); + const { result } = renderHook(() => useStorageValue(adapter, 'foo', 'default value')); expect(result.current[0]).toBe('bar'); act(() => { @@ -166,14 +147,12 @@ describe('useStorageValue', () => { }); it('should refetch value from store on fetchItem call', () => { - adapter.getItem.mockImplementationOnce(() => 'bar'); - const { result } = renderHook(() => - useStorageValue(adapter, 'foo', 'default value', { raw: true }) - ); + adapter.getItem.mockImplementationOnce(() => '"bar"'); + const { result } = renderHook(() => useStorageValue(adapter, 'foo', 'default value')); expect(adapter.getItem).toHaveBeenCalledTimes(1); expect(result.current[0]).toBe('bar'); - adapter.getItem.mockImplementationOnce(() => 'baz'); + adapter.getItem.mockImplementationOnce(() => '"baz"'); act(() => { result.current[3](); }); @@ -182,9 +161,9 @@ describe('useStorageValue', () => { }); it('should refetch value on key change', () => { - adapter.getItem.mockImplementation((key) => key); + adapter.getItem.mockImplementation((key) => `"${key}"`); const { result, rerender } = renderHook( - ({ key }) => useStorageValue(adapter, key, 'default value', { raw: true }), + ({ key }) => useStorageValue(adapter, key, 'default value'), { initialProps: { key: 'foo' } } ); @@ -197,20 +176,18 @@ describe('useStorageValue', () => { adapter.getItem.mockImplementationOnce(() => null); const { result } = renderHook(() => useStorageValue(adapter, 'foo', 'default value', { - raw: true, storeDefaultValue: true, }) ); expect(result.current[0]).toBe('default value'); - expect(adapter.setItem).toHaveBeenCalledWith('foo', 'default value'); + expect(adapter.setItem).toHaveBeenCalledWith('foo', '"default value"'); }); it('should store default value if it became default after initial render', () => { - adapter.getItem.mockImplementationOnce(() => 'bar'); + adapter.getItem.mockImplementationOnce(() => '"bar"'); const { result } = renderHook(() => useStorageValue(adapter, 'foo', 'default value', { - raw: true, storeDefaultValue: true, }) ); @@ -224,20 +201,19 @@ describe('useStorageValue', () => { }); expect(result.current[0]).toBe('default value'); - expect(adapter.setItem).toHaveBeenCalledWith('foo', 'default value'); + expect(adapter.setItem).toHaveBeenCalledWith('foo', '"default value"'); }); it('should not store default value on rerenders with persisted state', () => { adapter.getItem.mockImplementationOnce(() => null); const { result, rerender } = renderHook(() => useStorageValue(adapter, 'foo', 'default value', { - raw: true, storeDefaultValue: true, }) ); expect(result.current[0]).toBe('default value'); - expect(adapter.setItem).toHaveBeenCalledWith('foo', 'default value'); + expect(adapter.setItem).toHaveBeenCalledWith('foo', '"default value"'); rerender(); rerender(); rerender(); @@ -248,11 +224,214 @@ describe('useStorageValue', () => { adapter.getItem.mockImplementationOnce(() => null); renderHook(() => useStorageValue(adapter, 'foo', null, { - raw: true, storeDefaultValue: true, }) ); expect(adapter.setItem).not.toHaveBeenCalled(); }); + + describe('should handle window`s `storage` event', () => { + const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); + const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); + + beforeEach(() => { + addEventListenerSpy.mockClear(); + removeEventListenerSpy.mockClear(); + }); + + it('should subscribe to event on mount', () => { + renderHook(() => useStorageValue(adapter, 'foo')); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const subscribeCall = addEventListenerSpy.mock.calls.find((i) => i[0] === 'storage')!; + expect(subscribeCall).not.toBe(undefined); + expect(subscribeCall[1]).toBeInstanceOf(Function); + expect(subscribeCall[2]).toStrictEqual({ passive: true }); + }); + + it('should not subscribe to event on mount if synchronisations is disabled', () => { + renderHook(() => + useStorageValue(adapter, 'foo', null, { handleStorageEvent: false }) + ); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const subscribeCall = addEventListenerSpy.mock.calls.find((i) => i[0] === 'storage')!; + expect(subscribeCall).toBe(undefined); + }); + + it('should not resubscribe for event even if key has changed', () => { + const { rerender } = renderHook(({ key }) => useStorageValue(adapter, key), { + initialProps: { key: 'foo' }, + }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const subscribeCall = addEventListenerSpy.mock.calls.find((i) => i[0] === 'storage')!; + const storageEventHandler = subscribeCall[1]; + + addEventListenerSpy.mockClear(); + removeEventListenerSpy.mockClear(); + + rerender({ key: 'bar' }); + expect(removeEventListenerSpy.mock.calls.find((i) => i[1] === storageEventHandler)).toBe( + undefined + ); + }); + + it('should unsubscribe on unmount', () => { + const { unmount } = renderHook(() => useStorageValue(adapter, 'foo')); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const subscribeCall = addEventListenerSpy.mock.calls.find((i) => i[0] === 'storage')!; + const storageEventHandler = subscribeCall[1]; + + addEventListenerSpy.mockClear(); + removeEventListenerSpy.mockClear(); + + unmount(); + expect(removeEventListenerSpy.mock.calls.find((i) => i[1] === storageEventHandler)).not.toBe( + undefined + ); + }); + + it('should update state if managed key is updated, without calls to storage', () => { + const { result } = renderHook(() => useStorageValue(localStorage, 'foo')); + + expect(result.current[0]).toBe(null); + + localStorage.setItem('foo', 'bar'); + act(() => { + window.dispatchEvent( + new StorageEvent('storage', { key: 'foo', storageArea: localStorage, newValue: '"foo"' }) + ); + }); + + expect(result.current[0]).toBe('foo'); + localStorage.removeItem('foo'); + }); + + it('should not update data on event storage or key mismatch', () => { + const { result } = renderHook(() => useStorageValue(localStorage, 'foo')); + + expect(result.current[0]).toBe(null); + + act(() => { + window.dispatchEvent( + new StorageEvent('storage', { + key: 'foo', + storageArea: sessionStorage, + newValue: '"foo"', + }) + ); + }); + expect(result.current[0]).toBe(null); + + act(() => { + window.dispatchEvent( + new StorageEvent('storage', { + key: 'bar', + storageArea: localStorage, + newValue: 'foo', + }) + ); + }); + expect(result.current[0]).toBe(null); + + localStorage.removeItem('foo'); + }); + }); + + describe('synchronisation', () => { + it('should update state of all hooks managing same key in same storage', () => { + const { result: res } = renderHook(() => useStorageValue(localStorage, 'foo')); + + const { result: res1 } = renderHook(() => useStorageValue(localStorage, 'foo')); + + expect(res.current[0]).toBe(null); + expect(res1.current[0]).toBe(null); + + act(() => { + res.current[1]('bar'); + }); + expect(res.current[0]).toBe('bar'); + expect(res1.current[0]).toBe('bar'); + + act(() => { + res.current[2](); + }); + expect(res.current[0]).toBe(null); + expect(res1.current[0]).toBe(null); + + localStorage.setItem('foo', '"123"'); + act(() => { + res.current[3](); + }); + expect(res.current[0]).toBe('123'); + expect(res1.current[0]).toBe('123'); + localStorage.removeItem('foo'); + }); + + it('should not synchronize isolated hooks', () => { + const { result: res } = renderHook(() => useStorageValue(localStorage, 'foo')); + + const { result: res1 } = renderHook(() => + useStorageValue(localStorage, 'foo', null, { isolated: true }) + ); + + expect(res.current[0]).toBe(null); + expect(res1.current[0]).toBe(null); + + act(() => { + res.current[1]('bar'); + }); + expect(res.current[0]).toBe('bar'); + expect(res1.current[0]).toBe(null); + + act(() => { + res.current[2](); + }); + expect(res.current[0]).toBe(null); + expect(res1.current[0]).toBe(null); + + localStorage.setItem('foo', '"123"'); + act(() => { + res.current[3](); + }); + act(() => { + res.current[3](); + }); + expect(res.current[0]).toBe('123'); + expect(res1.current[0]).toBe(null); + localStorage.removeItem('foo'); + }); + + it('should not be synchronized by isolated hooks', () => { + const { result: res } = renderHook(() => useStorageValue(localStorage, 'foo')); + + const { result: res1 } = renderHook(() => + useStorageValue(localStorage, 'foo', null, { isolated: true }) + ); + + expect(res.current[0]).toBe(null); + expect(res1.current[0]).toBe(null); + + act(() => { + res1.current[1]('bar'); + }); + expect(res.current[0]).toBe(null); + expect(res1.current[0]).toBe('bar'); + + act(() => { + res1.current[2](); + }); + expect(res.current[0]).toBe(null); + expect(res1.current[0]).toBe(null); + + localStorage.setItem('foo', '"123"'); + act(() => { + res1.current[3](); + }); + expect(res.current[0]).toBe(null); + expect(res1.current[0]).toBe('123'); + localStorage.removeItem('foo'); + }); + }); }); diff --git a/tests/ssr/useLocalStorageValue.test.ts b/tests/ssr/useLocalStorageValue.test.ts index 89352592a..de0353067 100644 --- a/tests/ssr/useLocalStorageValue.test.ts +++ b/tests/ssr/useLocalStorageValue.test.ts @@ -7,6 +7,8 @@ describe('useLocalStorageValue', () => { }); it('should render', () => { - renderHook(() => useLocalStorageValue('foo')); + renderHook(() => { + useLocalStorageValue('foo'); + }); }); }); diff --git a/tests/ssr/useSessionStorageValue.test.ts b/tests/ssr/useSessionStorageValue.test.ts new file mode 100644 index 000000000..79be4b4c9 --- /dev/null +++ b/tests/ssr/useSessionStorageValue.test.ts @@ -0,0 +1,14 @@ +import { renderHook } from '@testing-library/react-hooks/server'; +import { useSessionStorageValue } from '../../src'; + +describe('useSessionStorageValue', () => { + it('should be defined', () => { + expect(useSessionStorageValue).toBeDefined(); + }); + + it('should render', () => { + renderHook(() => { + useSessionStorageValue('foo'); + }); + }); +}); diff --git a/tests/ssr/useStorageValue.test.ts b/tests/ssr/useStorageValue.test.ts index 3b208bd6a..451bee15a 100644 --- a/tests/ssr/useStorageValue.test.ts +++ b/tests/ssr/useStorageValue.test.ts @@ -1,21 +1,24 @@ -import { renderHook, act } from '@testing-library/react-hooks/server'; -import { useStorageValue } from '../../src'; +import { act, renderHook } from '@testing-library/react-hooks/server'; +import { useStorageValue } from '../../src/useStorageValue'; +import Mocked = jest.Mocked; describe('useStorageValue', () => { it('should be defined', () => { expect(useStorageValue).toBeDefined(); }); - const adapter = { - getItem: jest.fn(), - setItem: jest.fn(), - removeItem: jest.fn(), - }; + const adapter = ({ + getItem: jest.fn(() => null), + + setItem: jest.fn(() => {}), + + removeItem: jest.fn(() => {}), + } as unknown) as Mocked; beforeEach(() => { - adapter.getItem.mockReset(); - adapter.setItem.mockReset(); - adapter.removeItem.mockReset(); + adapter.getItem.mockClear().mockImplementation(() => null); + adapter.setItem.mockClear(); + adapter.removeItem.mockClear(); }); it('should render', () => { @@ -28,37 +31,65 @@ describe('useStorageValue', () => { expect(adapter.getItem).not.toHaveBeenCalled(); }); - it('should throw in case non-string value been set in raw mode', () => { - adapter.getItem.mockImplementationOnce(() => null); - const { result } = renderHook(() => - useStorageValue(adapter, 'foo', null, { raw: true }) - ); + it('should not set storage value on setState call', () => { + const { result } = renderHook(() => useStorageValue(adapter, 'foo')); - expect(() => { - act(() => { - // @ts-expect-error testing inappropriate usage - result.current[1](123); - }); - }).toThrow( - new TypeError('value has to be a string, define serializer or cast it to string manually') - ); + expect(result.current[0]).toBe(undefined); + act(() => { + result.current[1]('bar'); + }); + expect(result.current[0]).toBe(undefined); + expect(adapter.setItem).not.toHaveBeenCalled(); }); - it('should call storage`s removeItem on item remove', () => { + it('should not call storage`s removeItem on item remove', () => { adapter.getItem.mockImplementationOnce(() => null); const { result } = renderHook(() => useStorageValue(adapter, 'foo', null)); act(() => { result.current[2](); }); - expect(adapter.removeItem).toHaveBeenCalledWith('foo'); + expect(adapter.removeItem).not.toHaveBeenCalled(); + }); + + it('should not set state to default value on item remove', () => { + adapter.getItem.mockImplementationOnce(() => '"bar"'); + const { result } = renderHook(() => useStorageValue(adapter, 'foo', 'default value')); + + expect(result.current[0]).toBe(undefined); + act(() => { + result.current[2](); + }); + expect(result.current[0]).toBe(undefined); + }); + + it('should not re-fetch value from store on fetchItem call', () => { + adapter.getItem.mockImplementationOnce(() => '"bar"'); + const { result } = renderHook(() => useStorageValue(adapter, 'foo', 'default value')); + + expect(adapter.getItem).not.toHaveBeenCalled(); + act(() => { + result.current[3](); + }); + expect(adapter.getItem).not.toHaveBeenCalled(); + }); + + it('should not store initially default value to storage if configured', () => { + adapter.getItem.mockImplementationOnce(() => null); + const { result } = renderHook(() => + useStorageValue(adapter, 'foo', 'default value', { + storeDefaultValue: true, + }) + ); + + expect(result.current[0]).toBe(undefined); + expect(adapter.setItem).not.toHaveBeenCalled(); }); it('should not store null default value to store', () => { adapter.getItem.mockImplementationOnce(() => null); renderHook(() => useStorageValue(adapter, 'foo', null, { - raw: true, storeDefaultValue: true, }) ); diff --git a/yarn.lock b/yarn.lock index 3ff667113..96ba4d3df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,7 +55,28 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.1.0", "@babel/core@^7.12.10", "@babel/core@^7.13.16", "@babel/core@^7.7.5": +"@babel/core@^7.1.0", "@babel/core@^7.7.5": + version "7.14.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.2.tgz#54e45334ffc0172048e5c93ded36461d3ad4c417" + integrity sha512-OgC1mON+l4U4B4wiohJlQNUU3H73mpTyYY3j/c8U9dr9UagGGSm+WFpzjy/YLdoyjiG++c1kIDgxCo/mLwQJeQ== + dependencies: + "@babel/code-frame" "^7.12.13" + "@babel/generator" "^7.14.2" + "@babel/helper-compilation-targets" "^7.13.16" + "@babel/helper-module-transforms" "^7.14.2" + "@babel/helpers" "^7.14.0" + "@babel/parser" "^7.14.2" + "@babel/template" "^7.12.13" + "@babel/traverse" "^7.14.2" + "@babel/types" "^7.14.2" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.1.2" + semver "^6.3.0" + source-map "^0.5.0" + +"@babel/core@^7.12.10", "@babel/core@^7.13.16": version "7.14.0" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.0.tgz#47299ff3ec8d111b493f1a9d04bf88c04e728d88" integrity sha512-8YqpRig5NmIHlMLw09zMlPTvUVMILjqCOtVgu+TVNWEBvy9b5I3RRyhqnrV4hjgEK7n8P9OqvkWJAFmEL6Wwfw== @@ -76,7 +97,7 @@ semver "^6.3.0" source-map "^0.5.0" -"@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.14.0": +"@babel/generator@^7.12.11", "@babel/generator@^7.12.5": version "7.14.1" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.1.tgz#1f99331babd65700183628da186f36f63d615c93" integrity sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ== @@ -85,6 +106,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.14.0", "@babel/generator@^7.14.2": + version "7.14.2" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.2.tgz#d5773e8b557d421fd6ce0d5efa5fd7fc22567c30" + integrity sha512-OnADYbKrffDVai5qcpkMxQ7caomHOoEwjkouqnN2QhydAjowFAZcsdecFIRUBdb+ZcruwYE4ythYmF1UBZU5xQ== + dependencies: + "@babel/types" "^7.14.2" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.10.4", "@babel/helper-annotate-as-pure@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz#0f58e86dfc4bb3b1fcd7db806570e177d439b6ab" @@ -165,14 +195,14 @@ dependencies: "@babel/types" "^7.13.0" -"@babel/helper-function-name@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz#93ad656db3c3c2232559fd7b2c3dbdcbe0eb377a" - integrity sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA== +"@babel/helper-function-name@^7.12.13", "@babel/helper-function-name@^7.14.2": + version "7.14.2" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.14.2.tgz#397688b590760b6ef7725b5f0860c82427ebaac2" + integrity sha512-NYZlkZRydxw+YT56IlhIcS8PAhb+FEUiOzuhFTfqDyPmzAhRge6ua0dQYT/Uh0t/EDHq05/i+e5M2d4XvjgarQ== dependencies: "@babel/helper-get-function-arity" "^7.12.13" "@babel/template" "^7.12.13" - "@babel/types" "^7.12.13" + "@babel/types" "^7.14.2" "@babel/helper-get-function-arity@^7.12.13": version "7.12.13" @@ -203,7 +233,7 @@ dependencies: "@babel/types" "^7.13.12" -"@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.13.0", "@babel/helper-module-transforms@^7.14.0": +"@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.13.0": version "7.14.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.14.0.tgz#8fcf78be220156f22633ee204ea81f73f826a8ad" integrity sha512-L40t9bxIuGOfpIGA3HNkJhU9qYrf4y5A5LUSw7rGMSn+pcG8dfJ0g6Zval6YJGd2nEjI7oP00fRdnhLKndx6bw== @@ -217,6 +247,20 @@ "@babel/traverse" "^7.14.0" "@babel/types" "^7.14.0" +"@babel/helper-module-transforms@^7.14.0", "@babel/helper-module-transforms@^7.14.2": + version "7.14.2" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.14.2.tgz#ac1cc30ee47b945e3e0c4db12fa0c5389509dfe5" + integrity sha512-OznJUda/soKXv0XhpvzGWDnml4Qnwp16GN+D/kZIdLsWoHj05kyu8Rm5kXmMef+rVJZ0+4pSGLkeixdqNUATDA== + dependencies: + "@babel/helper-module-imports" "^7.13.12" + "@babel/helper-replace-supers" "^7.13.12" + "@babel/helper-simple-access" "^7.13.12" + "@babel/helper-split-export-declaration" "^7.12.13" + "@babel/helper-validator-identifier" "^7.14.0" + "@babel/template" "^7.12.13" + "@babel/traverse" "^7.14.2" + "@babel/types" "^7.14.2" + "@babel/helper-optimise-call-expression@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz#5c02d171b4c8615b1e7163f888c1c81c30a2aaea" @@ -312,7 +356,12 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.12.13", "@babel/parser@^7.12.7", "@babel/parser@^7.14.0": +"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.14.0", "@babel/parser@^7.14.2": + version "7.14.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.2.tgz#0c1680aa44ad4605b16cbdcc5c341a61bde9c746" + integrity sha512-IoVDIHpsgE/fu7eXBeRWt8zLbDrSvD7H1gpomOkPpBoEN8KCruCqSDdqo8dddwQQrui30KSvQBaMUOJiuFu6QQ== + +"@babel/parser@^7.12.11", "@babel/parser@^7.12.7": version "7.14.1" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.1.tgz#1bd644b5db3f5797c4479d89ec1817fe02b84c47" integrity sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q== @@ -1086,7 +1135,21 @@ "@babel/parser" "^7.12.13" "@babel/types" "^7.12.13" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.15", "@babel/traverse@^7.14.0": +"@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.14.0", "@babel/traverse@^7.14.2": + version "7.14.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.2.tgz#9201a8d912723a831c2679c7ebbf2fe1416d765b" + integrity sha512-TsdRgvBFHMyHOOzcP9S6QU0QQtjxlRpEYOy3mcCO5RgmC305ki42aSAmfZEMSSYBla2oZ9BMqYlncBaKmD/7iA== + dependencies: + "@babel/code-frame" "^7.12.13" + "@babel/generator" "^7.14.2" + "@babel/helper-function-name" "^7.14.2" + "@babel/helper-split-export-declaration" "^7.12.13" + "@babel/parser" "^7.14.2" + "@babel/types" "^7.14.2" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/traverse@^7.12.9", "@babel/traverse@^7.13.15": version "7.14.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.0.tgz#cea0dc8ae7e2b1dec65f512f39f3483e8cc95aef" integrity sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA== @@ -1100,7 +1163,15 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.12.1", "@babel/types@^7.12.13", "@babel/types@^7.12.7", "@babel/types@^7.13.0", "@babel/types@^7.13.12", "@babel/types@^7.13.16", "@babel/types@^7.14.0", "@babel/types@^7.14.1", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": +"@babel/types@^7.0.0", "@babel/types@^7.12.13", "@babel/types@^7.13.12", "@babel/types@^7.14.0", "@babel/types@^7.14.1", "@babel/types@^7.14.2", "@babel/types@^7.3.0", "@babel/types@^7.3.3": + version "7.14.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.2.tgz#4208ae003107ef8a057ea8333e56eb64d2f6a2c3" + integrity sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw== + dependencies: + "@babel/helper-validator-identifier" "^7.14.0" + to-fast-properties "^2.0.0" + +"@babel/types@^7.12.1", "@babel/types@^7.12.7", "@babel/types@^7.13.0", "@babel/types@^7.13.16", "@babel/types@^7.4.4": version "7.14.1" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.1.tgz#095bd12f1c08ab63eff6e8f7745fa7c9cc15a9db" integrity sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA== @@ -2897,9 +2968,9 @@ form-data "^3.0.0" "@types/node@*": - version "15.0.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-15.0.2.tgz#51e9c0920d1b45936ea04341aa3e2e58d339fb67" - integrity sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA== + version "15.3.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.3.0.tgz#d6fed7d6bc6854306da3dea1af9f874b00783e26" + integrity sha512-8/bnjSZD86ZfpBsDlCIkNXIvm+h6wi9g7IqL+kmFkQ+Wvu3JrasgLElfiPgoo8V8vVfnEi0QVS12gbl94h9YsQ== "@types/node@^14.0.10": version "14.14.44" @@ -4395,11 +4466,16 @@ camelcase@^6.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== -caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001219: +caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125: version "1.0.30001222" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001222.tgz#2789b8487282cbbe1700924f53951303d28086a9" integrity sha512-rPmwUK0YMjfMlZVmH6nVB5U3YJ5Wnx3vmT5lnRO3nIKO8bJ+TRWMbGuuiSugDJqESy/lz+1hSrlQEagCtoOAWQ== +caniuse-lite@^1.0.30001219: + version "1.0.30001228" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz#bfdc5942cd3326fa51ee0b42fbef4da9d492a7fa" + integrity sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A== + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -5646,11 +5722,16 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.564, electron-to-chromium@^1.3.723: +electron-to-chromium@^1.3.564: version "1.3.727" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.727.tgz#857e310ca00f0b75da4e1db6ff0e073cc4a91ddf" integrity sha512-Mfz4FIB4FSvEwBpDfdipRIrwd6uo8gUDoRDF4QEYb4h4tSuI3ov594OrjU6on042UlFHouIJpClDODGkPcBSbg== +electron-to-chromium@^1.3.723: + version "1.3.728" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.728.tgz#dbedd6373f595ae10a13d146b66bece4c1afa5bd" + integrity sha512-SHv4ziXruBpb1Nz4aTuqEHBYi/9GNCJMYIJgDEXrp/2V01nFXMNFUTli5Z85f5ivSkioLilQatqBYFB44wNJrA== + element-resize-detector@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.2.2.tgz#bf7c3ff915957e4e62e86241ed2f9c86b078892b" @@ -6958,7 +7039,7 @@ glob-to-regexp@^0.3.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= -glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@^7.0.0, glob@^7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -6970,6 +7051,18 @@ glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-dirs@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" @@ -7778,9 +7871,9 @@ is-cidr@^4.0.2: cidr-regex "^3.1.1" is-core-module@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.3.0.tgz#d341652e3408bca69c4671b79a0954a3d349f887" - integrity sha512-xSphU2KG9867tsYdLD4RWQ1VqdFl4HTO9Thf3I/3dLEfr0dbPTWKsuCKrgqMljg4nPE+Gq0VCnzT3gr0CyBmsw== + version "2.4.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1" + integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A== dependencies: has "^1.0.3" @@ -9852,11 +9945,16 @@ node-notifier@^8.0.0: uuid "^8.3.0" which "^2.0.2" -node-releases@^1.1.61, node-releases@^1.1.71: +node-releases@^1.1.61: version "1.1.71" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== +node-releases@^1.1.71: + version "1.1.72" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.72.tgz#14802ab6b1039a79a0c7d662b610a5bbd76eacbe" + integrity sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw== + nopt@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" @@ -13682,9 +13780,9 @@ v8-compile-cache@^2.0.3: integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== v8-to-istanbul@^7.0.0: - version "7.1.1" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.1.tgz#04bfd1026ba4577de5472df4f5e89af49de5edda" - integrity sha512-p0BB09E5FRjx0ELN6RgusIPsSPhtgexSRcKETybEs6IGOTXJSZqfwxp7r//55nnu0f1AxltY5VvdVqy2vZf9AA== + version "7.1.2" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz#30898d1a7fa0c84d225a2c1434fb958f290883c1" + integrity sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow== dependencies: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0"