@gilbarbara/hooks
Version:
Collection of useful React hooks
1 lines • 77.8 kB
Source Map (JSON)
{"version":3,"sources":["../src/useBreakpoint.tsx","../src/utils.ts","../src/useCallbackDeepCompare.ts","../src/useClickOutside.ts","../src/useMemoizedValue.ts","../src/useEffectDeepCompare.ts","../src/useIsFirstRender.ts","../src/useDataChanges.tsx","../src/useUpdateEffect.ts","../src/useDebounce.ts","../src/useEffectOnce.ts","../src/useElementMeasure.tsx","../src/defaults.ts","../src/useIsomorphicLayoutEffect.ts","../src/useResizeObserver.ts","../src/useFetch.ts","../src/useIsMounted.ts","../src/usePrevious.ts","../src/useSetState.ts","../src/useHasChanged.ts","../src/useIntersectionObserver.ts","../src/useInterval.ts","../src/useLatest.ts","../src/useLifecycleHooks.ts","../src/useLocalStorage.ts","../src/useLocation.ts","../src/useMediaQuery.ts","../src/useMemoDeepCompare.ts","../src/useMergeRefs.tsx","../src/useMount.ts","../src/useOnce.tsx","../src/usePersistentState.ts","../src/useRenderCount.tsx","../src/useScript.tsx","../src/useThrottle.ts","../src/useUnmount.ts","../src/useThrottleValue.ts","../src/useTimeout.ts","../src/useToggle.ts","../src/useUpdate.ts","../src/useWindowSize.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react';\n\nimport { canUseDOM, off, on } from './utils';\n\nexport type Breakpoints = Record<string, number>;\n\nexport type UseBreakpointOrientation = 'landscape' | 'portrait';\n\nexport interface UseBreakpointResult<T extends Breakpoints> {\n /**\n * Returns true if the screen size is between the specified breakpoints and matches the optional orientation.\n */\n between(min: keyof T, max: keyof T, andOrientation?: UseBreakpointOrientation): boolean;\n /**\n * Returns true if the screen size is less than or equal to the specified breakpoint and matches the optional orientation.\n */\n max(breakpoint: keyof T, andOrientation?: UseBreakpointOrientation): boolean;\n /**\n * Returns true if the screen size is greater than or equal to the specified breakpoint and matches the optional orientation.\n */\n min(breakpoint: keyof T, andOrientation?: UseBreakpointOrientation): boolean;\n /**\n * The current screen orientation, either portrait or landscape.\n */\n orientation: UseBreakpointOrientation;\n /**\n * The current active breakpoint, based on the defined breakpoints.\n */\n size: keyof T;\n}\n\nconst defaultBreakpoints: Breakpoints = { xs: 0, sm: 400, md: 768, lg: 1024, xl: 1280 };\n\nexport function useBreakpoint<T extends Breakpoints>(\n customBreakpoints?: T,\n initialWidth = Infinity,\n initialHeight = Infinity,\n): UseBreakpointResult<T> {\n const breakpoints = (customBreakpoints ?? defaultBreakpoints) as T;\n\n const sizes = useMemo(\n () => Object.entries(breakpoints).sort(([, aSize], [, bSize]) => bSize - aSize),\n [breakpoints],\n );\n\n // Ensure the smallest breakpoint is 0 and warn if it's not\n const smallestBreakpoint = sizes[sizes.length - 1];\n\n if (smallestBreakpoint[1] !== 0) {\n if (process.env.NODE_ENV !== 'production') {\n // eslint-disable-next-line no-console\n console.warn(`The \"${smallestBreakpoint[0]}\" breakpoint should be 0`);\n }\n }\n\n const getScreen = useCallback((): UseBreakpointResult<T> => {\n const height = canUseDOM() ? window.innerHeight : initialHeight;\n const width = canUseDOM() ? window.innerWidth : initialWidth;\n const size = sizes.find(([, s]) => s <= width) || sizes[0];\n const orientation = width > height ? 'landscape' : 'portrait';\n\n return {\n between: (min, max, andOrientation) =>\n width >= breakpoints[min] &&\n width < breakpoints[max] &&\n (!andOrientation || andOrientation === orientation),\n min: (breakpoint, andOrientation) =>\n width >= breakpoints[breakpoint] && (!andOrientation || andOrientation === orientation),\n max: (breakpoint, andOrientation) =>\n width < breakpoints[breakpoint] && (!andOrientation || andOrientation === orientation),\n orientation,\n size: size[0] as keyof T,\n };\n }, [breakpoints, initialHeight, initialWidth, sizes]);\n\n const [screen, setScreen] = useState(getScreen);\n\n useEffect(() => {\n const onResize = () => {\n setScreen(previous => {\n const current = getScreen();\n\n return current.size !== previous.size || current.orientation !== previous.orientation\n ? current\n : previous;\n });\n };\n\n on(window, 'resize', onResize);\n\n return () => off(window, 'resize', onResize);\n }, [getScreen]);\n\n return screen;\n}\n","import { DependencyList } from 'react';\n\nimport { PlainObject, Primitive, Target } from './types';\n\nexport function canUseDOM() {\n return !!(typeof window !== 'undefined' && window?.document?.createElement);\n}\n\nexport function delay(delayMs: number) {\n return new Promise<void>(resolve => {\n setTimeout(resolve, delayMs);\n });\n}\n\nexport function getElement<T extends Element>(target: Target<T>) {\n if (!canUseDOM()) {\n return null;\n }\n\n let targetEl: Element | null;\n\n if (typeof target === 'string') {\n targetEl = document.querySelector(target);\n } else {\n targetEl = target && 'current' in target ? target.current : target;\n }\n\n return targetEl;\n}\n\nexport function isPlainObject(value: unknown): value is PlainObject {\n if (Object.prototype.toString.call(value) !== '[object Object]') {\n return false;\n }\n\n const prototype = Object.getPrototypeOf(value);\n\n return prototype === null || prototype === Object.getPrototypeOf({});\n}\n\nexport function isPrimitive(value: any): value is Primitive {\n return value !== Object(value);\n}\n\nexport function isString(value: unknown): value is string {\n return typeof value === 'string';\n}\n\nexport function isURL(value: unknown): value is string {\n if (!isString(value)) {\n return false;\n }\n\n try {\n new URL(value); // eslint-disable-line no-new\n\n return true;\n } catch {\n return false;\n }\n}\n\nexport function noop() {\n return undefined;\n}\n\n/* eslint-disable @typescript-eslint/no-unsafe-function-type */\nexport function off<T extends Window | Document | HTMLElement | EventTarget>(\n target: T | null,\n ...rest: Parameters<T['removeEventListener']> | [string, Function | null, ...any]\n): void {\n if (target && target.removeEventListener) {\n target.removeEventListener(...(rest as Parameters<HTMLElement['removeEventListener']>));\n }\n}\n\nexport function on<T extends Window | Document | HTMLElement | EventTarget>(\n target: T | null,\n ...rest: Parameters<T['addEventListener']> | [string, Function | null, ...any]\n): void {\n if (target && target.addEventListener) {\n target.addEventListener(...(rest as Parameters<HTMLElement['addEventListener']>));\n }\n}\n/* eslint-enable @typescript-eslint/no-unsafe-function-type */\n\nexport function validateDependencies(dependencies: DependencyList, name: string, fallback: string) {\n if (process.env.NODE_ENV !== 'production') {\n if (!(dependencies instanceof Array) || !dependencies.length) {\n // eslint-disable-next-line no-console\n console.warn(\n `${name} should not be used with no dependencies. Use React.${fallback} instead.`,\n );\n }\n\n if (dependencies.length && dependencies.every(isPrimitive)) {\n // eslint-disable-next-line no-console\n console.warn(\n `${name} should not be used with dependencies that are all primitive values. Use React.${fallback} instead.`,\n );\n }\n }\n}\n","import { DependencyList, useCallback, useRef } from 'react';\nimport deepEqual from '@gilbarbara/deep-equal';\n\nimport { FunctionWithArguments } from './types';\nimport { validateDependencies } from './utils';\n\nexport function useCallbackDeepCompare<T extends FunctionWithArguments>(\n callback: T,\n dependencies: DependencyList,\n): T {\n validateDependencies(dependencies, 'useCallbackDeepCompare', 'useCallback');\n\n const ref = useRef<DependencyList>(dependencies);\n const callbackRef = useRef<T>(callback); // Store the latest function reference\n\n if (!deepEqual(dependencies, ref.current)) {\n ref.current = dependencies;\n callbackRef.current = callback; // Update function reference when deps change\n }\n\n return useCallback(\n (...arguments_: any[]) => callbackRef.current(...arguments_),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n ref.current,\n ) as T;\n}\n","import { useEffect, useRef } from 'react';\n\nimport { useMemoizedValue } from './useMemoizedValue';\nimport { off, on } from './utils';\n\nexport function useClickOutside<T extends Element = HTMLElement>(callback: () => void) {\n const ref = useRef<T>(null);\n const memoizedCallback = useMemoizedValue(callback);\n\n useEffect(() => {\n const handleClick = (event: MouseEvent | TouchEvent) => {\n if (!ref.current?.contains(event.target as Node)) {\n memoizedCallback();\n }\n };\n\n on(document, 'click', handleClick);\n\n return () => {\n off(document, 'click', handleClick);\n };\n }, [memoizedCallback]);\n\n return ref;\n}\n","import { useState } from 'react';\n\nimport { useEffectDeepCompare } from './useEffectDeepCompare';\nimport { useIsFirstRender } from './useIsFirstRender';\n\nexport function useMemoizedValue<T = any>(value: T) {\n const [stableValue, setStableValue] = useState(() => value);\n const isFirstRender = useIsFirstRender();\n\n useEffectDeepCompare(() => {\n if (isFirstRender) {\n return;\n }\n\n setStableValue(() => value);\n }, [value]);\n\n return stableValue;\n}\n","import { DependencyList, EffectCallback, useEffect, useRef } from 'react';\nimport deepEqual from '@gilbarbara/deep-equal';\n\nimport { validateDependencies } from './utils';\n\nexport function useEffectDeepCompare(effect: EffectCallback, dependencies: DependencyList) {\n validateDependencies(dependencies, 'useEffectDeepCompare', 'useEffect');\n\n const ref = useRef<DependencyList>(dependencies);\n\n if (!deepEqual(dependencies, ref.current)) {\n ref.current = dependencies;\n }\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useEffect(effect, ref.current);\n}\n","import { useRef } from 'react';\n\nexport function useIsFirstRender(): boolean {\n const isFirstRender = useRef(true);\n\n if (isFirstRender.current) {\n isFirstRender.current = false;\n\n return true;\n }\n\n return isFirstRender.current;\n}\n","import { useRef, useState } from 'react';\nimport deepEqual from '@gilbarbara/deep-equal';\n\nimport { PlainObject } from './types';\nimport { useMemoizedValue } from './useMemoizedValue';\nimport { useUpdateEffect } from './useUpdateEffect';\nimport { isString } from './utils';\n\nexport type UseDataChangesResult<T> = {\n [K in keyof T]?: {\n from: any;\n to: any;\n };\n};\n\nexport interface UseDataChangesOptions<T> {\n /**\n * Determines whether to use shallow or deep comparison when checking for changes.\n * - `'shallow'` (default) follows React's behavior and only checks for reference changes.\n * - `'deep'` performs a deep comparison to detect nested changes.\n */\n comparison?: 'shallow' | 'deep';\n /**\n * An optional name to include in the console log for easier debugging.\n */\n name?: string;\n /**\n * An array of specific keys to track changes for. If omitted, all keys are tracked.\n */\n only?: Array<keyof T>;\n /**\n * Suppresses console logging when changes are detected.\n *\n * Only needed if `onChange` is **not** used.\n */\n skipLog?: boolean;\n}\n\nexport function useDataChanges<T extends PlainObject<any>>(\n data: T,\n nameOrOptions: string | UseDataChangesOptions<T> = {},\n): UseDataChangesResult<T> | undefined {\n const previousData = useRef<T>(data);\n const [changes, setChanges] = useState<UseDataChangesResult<T> | undefined>(undefined);\n\n const {\n comparison = 'shallow',\n name,\n only,\n skipLog = false,\n } = useMemoizedValue(isString(nameOrOptions) ? { name: nameOrOptions } : nameOrOptions);\n\n useUpdateEffect(() => {\n // Determine which keys to check\n const keysToCheck =\n only ?? (Object.keys({ ...previousData.current, ...data }) as Array<keyof T>);\n const changesObject: UseDataChangesResult<T> = {};\n\n keysToCheck.forEach(key => {\n const hasChanged =\n comparison === 'deep'\n ? !deepEqual(previousData.current[key], data[key])\n : previousData.current[key] !== data[key];\n\n if (hasChanged) {\n changesObject[key] = {\n from: previousData.current[key],\n to: data[key],\n };\n }\n });\n\n const hasChanges = Object.keys(changesObject).length > 0;\n\n setChanges(hasChanges ? changesObject : undefined);\n\n if (hasChanges && !skipLog) {\n const nameToken = name ? `: ${name}` : '';\n\n // eslint-disable-next-line no-console\n console.log(`[data-changes${nameToken}]`, changesObject);\n }\n\n previousData.current = data;\n }, [name, data, only, comparison, skipLog]);\n\n return changes;\n}\n","import { DependencyList, EffectCallback, useEffect } from 'react';\n\nimport { useIsFirstRender } from './useIsFirstRender';\n\nexport function useUpdateEffect(effect: EffectCallback, dependencies?: DependencyList) {\n const isFirstRender = useIsFirstRender();\n\n useEffect(() => {\n if (!isFirstRender) {\n return effect();\n }\n\n return undefined;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, dependencies);\n}\n","import { DependencyList, useCallback, useEffect, useRef } from 'react';\n\nimport { TimerStatus } from './types';\nimport { useEffectDeepCompare } from './useEffectDeepCompare';\nimport { useIsFirstRender } from './useIsFirstRender';\n\nexport type UseDebounceStatus = TimerStatus;\n\n// export it\nexport interface UseDebounceResult {\n cancel: () => void;\n getStatus: () => UseDebounceStatus;\n}\n\nexport function useDebounce(\n callback: () => void,\n delayMs: number = 250,\n dependencies: DependencyList = [],\n): UseDebounceResult {\n const status = useRef<UseDebounceStatus>('pending');\n const timeout = useRef<ReturnType<typeof setTimeout>>(null);\n const savedCallback = useRef(callback);\n const isFirstRender = useIsFirstRender();\n\n const clear = useCallback(() => {\n status.current = 'cancelled';\n timeout.current && clearTimeout(timeout.current);\n }, []);\n\n const set = useCallback(() => {\n status.current = 'pending';\n timeout.current && clearTimeout(timeout.current);\n\n timeout.current = setTimeout(() => {\n status.current = 'completed';\n savedCallback.current();\n }, delayMs);\n }, [delayMs]);\n\n const getStatus = useCallback(() => status.current, []);\n\n useEffect(() => {\n savedCallback.current = callback;\n }, [callback]);\n\n useEffectDeepCompare(() => {\n if (!isFirstRender) {\n set();\n\n return clear;\n }\n\n return undefined;\n }, [set, clear, dependencies, delayMs]);\n\n return { cancel: clear, getStatus };\n}\n","import { EffectCallback, useEffect, useRef } from 'react';\n\nexport function useEffectOnce(effect: EffectCallback) {\n const destroyFn = useRef<ReturnType<EffectCallback> | null>(null);\n const effectCalled = useRef(false);\n const effectFn = useRef(effect);\n\n useEffect(() => {\n if (!effectCalled.current) {\n destroyFn.current = effectFn.current();\n effectCalled.current = true;\n }\n\n return () => {\n if (destroyFn.current) {\n destroyFn.current();\n destroyFn.current = null;\n }\n };\n }, []);\n}\n","import { useState } from 'react';\n\nimport { defaultElementDimensions } from './defaults';\nimport { Target } from './types';\nimport { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';\nimport { useResizeObserver } from './useResizeObserver';\nimport { canUseDOM, getElement } from './utils';\n\nexport interface UseElementMeasureResult extends Omit<DOMRectReadOnly, 'toJSON'> {\n absoluteHeight: number;\n absoluteWidth: number;\n}\n\nfunction getElementMeasure(element: Element | null): UseElementMeasureResult {\n if (!canUseDOM() || !element) {\n return defaultElementDimensions;\n }\n\n const { bottom, height, left, right, top, width, x, y } = element.getBoundingClientRect();\n\n const {\n borderBottom,\n borderLeft,\n borderRight,\n borderTop,\n paddingBottom,\n paddingLeft,\n paddingRight,\n paddingTop,\n } = getComputedStyle(element);\n\n return {\n absoluteHeight:\n height -\n parseFloatValue(paddingTop) -\n parseFloatValue(paddingBottom) -\n parseFloatValue(borderTop) -\n parseFloatValue(borderBottom),\n absoluteWidth:\n width -\n parseFloatValue(paddingLeft) -\n parseFloatValue(paddingRight) -\n parseFloatValue(borderLeft) -\n parseFloatValue(borderRight),\n bottom,\n height,\n left,\n right,\n top,\n width,\n x,\n y,\n };\n}\n\nfunction parseFloatValue(value: string) {\n const parsed = parseFloat(value);\n\n return Number.isNaN(parsed) ? 0 : parsed;\n}\n\nexport function useElementMeasure<T extends Element>(\n target: Target<T>,\n debounce = 0,\n): UseElementMeasureResult {\n const [element, setElement] = useState(getElement(target));\n const [dimensions, setDimensions] = useState<UseElementMeasureResult>(getElementMeasure(element));\n\n const entry = useResizeObserver(element, debounce);\n\n useIsomorphicLayoutEffect(() => {\n const nextElement = getElement(target);\n\n setElement(nextElement);\n setDimensions(getElementMeasure(nextElement));\n }, [target]);\n\n useIsomorphicLayoutEffect(() => {\n if (!entry) {\n return;\n }\n\n const { bottom, height, left, right, top, width, x, y } = entry.contentRect;\n const { blockSize, inlineSize } = entry.borderBoxSize[0];\n\n setDimensions({\n absoluteHeight: blockSize,\n absoluteWidth: inlineSize,\n bottom,\n height,\n left,\n right,\n top,\n width,\n x,\n y,\n });\n }, [entry]);\n\n return dimensions;\n}\n","import type { UseElementMeasureResult } from './useElementMeasure';\n\nexport const defaultElementDimensions: UseElementMeasureResult = {\n absoluteHeight: 0,\n absoluteWidth: 0,\n bottom: 0,\n height: 0,\n left: 0,\n right: 0,\n top: 0,\n width: 0,\n x: 0,\n y: 0,\n};\n","import { useEffect, useLayoutEffect } from 'react';\n\nimport { canUseDOM } from './utils';\n\nexport const useIsomorphicLayoutEffect = canUseDOM() ? useLayoutEffect : useEffect;\n","import { useMemo, useRef, useState } from 'react';\n\nimport { Target } from './types';\nimport { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';\nimport { canUseDOM, getElement } from './utils';\n\nexport function useResizeObserver<T extends Element>(target: Target<T>, debounce: number = 0) {\n const [element, setElement] = useState(getElement(target));\n const [value, setValue] = useState<ResizeObserverEntry>();\n const timeoutRef = useRef<number | null>(null);\n\n const isFirstCall = useRef(true);\n\n const observer = useMemo(() => {\n if (!canUseDOM()) {\n return {};\n }\n\n return new window.ResizeObserver(entries => {\n if (debounce && !isFirstCall.current) {\n if (timeoutRef.current) {\n window.clearTimeout(timeoutRef.current);\n }\n\n timeoutRef.current = window.setTimeout(() => {\n setValue(entries[0]);\n }, debounce);\n\n return;\n }\n\n setValue(entries[0]);\n isFirstCall.current = false;\n });\n }, [debounce]);\n\n useIsomorphicLayoutEffect(() => {\n setElement(getElement(target));\n }, [target]);\n\n useIsomorphicLayoutEffect(() => {\n if (!canUseDOM() || !(observer instanceof ResizeObserver)) {\n return () => undefined;\n }\n\n if (!element) {\n return () => undefined;\n }\n\n observer.observe(element);\n\n return () => {\n observer.disconnect();\n };\n }, [element, observer]);\n\n return value;\n}\n","import { useCallback, useEffect, useMemo } from 'react';\n\nimport { PlainObject } from './types';\nimport { useIsMounted } from './useIsMounted';\nimport { useMemoizedValue } from './useMemoizedValue';\nimport { usePrevious } from './usePrevious';\nimport { useSetState } from './useSetState';\nimport { isPlainObject, isURL } from './utils';\n\nexport const USE_FETCH_STATUS = {\n IDLE: 'IDLE',\n LOADING: 'LOADING',\n SUCCESS: 'SUCCESS',\n ERROR: 'ERROR',\n} as const;\n\ninterface UseFetchError extends Error {\n response?: unknown;\n status?: number;\n}\n\ninterface UseFetchState<TDataType> {\n data?: TDataType;\n error?: UseFetchError;\n isCached: boolean;\n retryCount: number | null;\n status: UseFetchStatus;\n url: string;\n}\n\nexport type UseFetchStatus = keyof typeof USE_FETCH_STATUS;\n\nexport interface UseFetchOptions<TDataType = unknown> {\n body?: BodyInit | Record<string, any>;\n /**\n * Time to cache the request if provided.\n * When the cache is expired, the request will be triggered again.\n */\n cacheTTL?: number;\n headers?: PlainObject<string>;\n /**\n * HTTP method.\n * @default: 'GET'\n */\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE';\n /**\n * Request mode.\n * @default: 'cors'\n */\n mode?: 'cors' | 'navigate' | 'no-cors' | 'same-origin';\n /** Callback fired when an error occurs */\n onError?: (error: UseFetchError) => void;\n /** Callback fired when the request completes (success or error) */\n onFinally?: () => void;\n /** Callback fired when the loading state changes */\n onLoading?: () => void;\n /** Callback fired when data is successfully fetched */\n onSuccess?: (data: TDataType) => void;\n /**\n * Number of retries.\n */\n retries?: number;\n /**\n * Time to wait before retrying.\n * A function like attempt => Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000) applies exponential backoff.\n * A function like attempt => attempt * 1000 applies linear backoff.\n * @default: attempt => attempt * 1000\n */\n retryDelay?: number | ((attempt: number) => number);\n /**\n * Request type.\n * @default: 'json'\n */\n type?: 'json' | 'urlencoded';\n url: string;\n /**\n * Wait for the user to trigger the request.\n * @default: false\n */\n wait?: boolean;\n}\n\nexport interface UseFetchResult<TDataType> extends UseFetchState<TDataType> {\n /**\n * Whether the response is cached.\n */\n isCached: boolean;\n isError: () => boolean;\n isFetched: () => boolean;\n isLoading: () => boolean;\n isPaused: () => boolean;\n isSuccess: () => boolean;\n refetch: (eraseData?: boolean) => void;\n}\n\nconst globalCache = new Map<string, { data: any; expiry: number }>();\n\nasync function request(options: UseFetchOptions): Promise<any> {\n const {\n body = undefined,\n headers = {},\n method = 'GET',\n mode = 'cors',\n type = 'json',\n url = '',\n } = options;\n\n const contentTypes = {\n json: 'application/json',\n urlencoded: 'application/x-www-form-urlencoded',\n };\n\n const params: RequestInit = {\n body: undefined,\n cache: 'no-store' as const,\n headers: {\n Accept: 'application/json',\n 'Content-Type': contentTypes[type],\n ...headers,\n },\n method,\n mode,\n credentials: undefined,\n };\n\n if (body) {\n params.body = (\n isPlainObject(body) && type === 'json' ? JSON.stringify(body) : body\n ) as BodyInit;\n }\n\n try {\n const response = await fetch(url, params);\n let content: unknown;\n\n try {\n content = await response.json();\n } catch {\n content = await response.text();\n }\n\n if (response.status > 299) {\n const error = new Error(response.statusText) as UseFetchError;\n\n error.status = response.status;\n error.response = content;\n\n throw error;\n }\n\n return content;\n } catch (error) {\n // Handle hard failures (network issues, CORS errors, fetch rejection)\n const fetchError = new Error('Network request failed') as UseFetchError;\n\n fetchError.status = 0; // Status 0 indicates a network error\n fetchError.response = error instanceof Error ? error.message : error;\n throw fetchError;\n }\n}\n\nexport const useFetchCache = {\n /**\n * Retrieve data from the cache.\n * @param url The URL to retrieve data for.\n * @returns Cached data or `undefined` if not found or expired.\n */\n get(url: string) {\n const cached = globalCache.get(url);\n\n if (cached && Date.now() < cached.expiry) {\n return cached;\n }\n\n globalCache.delete(url); // Remove expired entry\n\n return undefined;\n },\n\n /**\n * Set data in the cache.\n * @param url The URL to cache data for.\n * @param data The data to cache.\n * @param ttl Time-to-live in milliseconds.\n */\n set(url: string, data: any, ttl: number) {\n globalCache.set(url, { data, expiry: Date.now() + ttl });\n },\n\n /**\n * Clear the cache.\n * @param url Optional URL to clear. Clears all cache if not specified.\n */\n clear(url?: string) {\n if (url) {\n globalCache.delete(url);\n } else {\n globalCache.clear();\n }\n },\n\n /**\n * Check if a URL is cached and still valid.\n * @param url The URL to check.\n * @returns `true` if cached and valid, `false` otherwise.\n */\n has(url: string) {\n const cached = globalCache.get(url);\n\n return !!cached && Date.now() < cached.expiry;\n },\n};\n\nexport function useFetch<TDataType = unknown>(\n urlOrOptions: string | UseFetchOptions<TDataType>,\n): UseFetchResult<TDataType> {\n const {\n cacheTTL = 0,\n onError,\n onFinally,\n onLoading,\n onSuccess,\n retries = 0,\n retryDelay = (attempt: number) => attempt * 1000,\n wait = false,\n ...options\n } = useMemoizedValue(\n (isURL(urlOrOptions)\n ? ({ type: 'json', url: urlOrOptions } as const)\n : urlOrOptions) as UseFetchOptions,\n );\n const [{ data, error, isCached, retryCount, status, url }, setState] = useSetState<\n UseFetchState<TDataType>\n >({\n data: undefined,\n error: undefined,\n isCached: false,\n retryCount: null,\n status: USE_FETCH_STATUS.IDLE,\n url: options.url,\n });\n const isMounted = useIsMounted();\n const previousRetryCount = usePrevious(retryCount);\n\n if (!isPlainObject(options) || !isURL(url)) {\n throw new Error('Expected an options object or URL');\n }\n\n const getData = useCallback(\n (eraseData?: boolean) => {\n setState(s => ({\n data: eraseData ? undefined : s.data,\n error: undefined,\n isCached: false,\n status: USE_FETCH_STATUS.LOADING,\n }));\n\n onLoading?.();\n\n // Check cache\n if (cacheTTL && useFetchCache.has(url) && !eraseData) {\n const cached = useFetchCache.get(url);\n\n if (cached) {\n setState({\n data: cached.data,\n error: undefined,\n isCached: true,\n status: USE_FETCH_STATUS.SUCCESS,\n });\n\n onSuccess?.(cached.data);\n onFinally?.();\n\n return;\n }\n }\n\n // eslint-disable-next-line promise/catch-or-return\n request({ ...options })\n .then(response => {\n if (!isMounted()) {\n return;\n }\n\n if (cacheTTL) {\n useFetchCache.set(url, response, cacheTTL);\n }\n\n setState({\n data: response,\n retryCount: null,\n status: USE_FETCH_STATUS.SUCCESS,\n });\n onSuccess?.(response);\n })\n .catch(responseError => {\n if (!isMounted()) {\n return;\n }\n\n const counter = retryCount ?? 0;\n\n if (!retries || counter >= retries) {\n setState({\n error: responseError,\n status: USE_FETCH_STATUS.ERROR,\n });\n onError?.(responseError);\n }\n\n if (retries && counter < retries) {\n setState({\n retryCount: counter + 1,\n });\n }\n })\n .finally(() => {\n if (isMounted()) {\n onFinally?.();\n }\n });\n },\n [\n cacheTTL,\n isMounted,\n onError,\n onFinally,\n onLoading,\n onSuccess,\n options,\n retries,\n retryCount,\n setState,\n url,\n ],\n );\n\n useEffect(() => {\n if (url !== options.url) {\n setState({\n data: undefined,\n error: undefined,\n status: USE_FETCH_STATUS.IDLE,\n url: options.url,\n });\n }\n }, [options.url, setState, url]);\n\n useEffect(() => {\n if (status === USE_FETCH_STATUS.IDLE && !wait) {\n getData();\n }\n }, [getData, status, wait]);\n\n useEffect(() => {\n if (retries && typeof retryCount === 'number' && retryCount !== previousRetryCount) {\n setTimeout(getData, typeof retryDelay === 'function' ? retryDelay(retryCount) : retryDelay);\n }\n }, [getData, previousRetryCount, retries, retryCount, retryDelay]);\n\n const isError = useCallback(() => status === USE_FETCH_STATUS.ERROR, [status]);\n const isFetched = useCallback(\n () => ([USE_FETCH_STATUS.SUCCESS, USE_FETCH_STATUS.ERROR] as UseFetchStatus[]).includes(status),\n [status],\n );\n const isLoading = useCallback(() => status === USE_FETCH_STATUS.LOADING, [status]);\n const isPaused = useCallback(() => status === USE_FETCH_STATUS.IDLE && wait, [status, wait]);\n const isSuccess = useCallback(() => status === USE_FETCH_STATUS.SUCCESS, [status]);\n const refetch = useCallback((eraseData = false) => getData(eraseData), [getData]);\n\n return useMemo(\n () => ({\n data,\n error,\n isCached,\n isError,\n isFetched,\n isLoading,\n isPaused,\n isSuccess,\n refetch,\n retryCount,\n status,\n url,\n }),\n [\n data,\n error,\n isCached,\n isError,\n isFetched,\n isLoading,\n isPaused,\n isSuccess,\n refetch,\n retryCount,\n status,\n url,\n ],\n );\n}\n","import { useCallback, useEffect, useRef } from 'react';\n\nexport function useIsMounted() {\n const isMounted = useRef(false);\n\n useEffect(() => {\n isMounted.current = true;\n\n return () => {\n isMounted.current = false;\n };\n }, []);\n\n return useCallback(() => isMounted.current, []);\n}\n","import { useEffect, useRef } from 'react';\n\nexport function usePrevious<T>(state: T): T | undefined {\n const ref = useRef<T>(undefined);\n\n useEffect(() => {\n ref.current = state;\n });\n\n return ref.current;\n}\n","import { useCallback, useState } from 'react';\n\ntype Patch<T> = Partial<T> | ((previousState: T) => Partial<T>);\n\nexport function useSetState<T extends object>(\n initialState: T = {} as T,\n): [T, (patch: Patch<T>) => void] {\n const [state, set] = useState<T>(initialState);\n const setState = useCallback((patch: Patch<T>) => {\n set(previousState => ({\n ...previousState,\n ...(patch instanceof Function ? patch(previousState) : patch),\n }));\n }, []);\n\n return [state, setState];\n}\n","import { useEffect } from 'react';\n\nimport { usePrevious } from './usePrevious';\n\nexport function useHasChanged<T>(\n value: T,\n callback?: (previous: T) => void,\n): [hasChanged: boolean, previous: T | undefined] {\n const previous = usePrevious(value);\n const hasChanged = typeof previous !== 'undefined' && previous !== value;\n\n useEffect(() => {\n if (hasChanged) {\n callback?.(previous);\n }\n }, [callback, hasChanged, previous]);\n\n return [hasChanged, previous];\n}\n","import { useEffect, useMemo, useState } from 'react';\n\nimport { Target } from './types';\nimport { canUseDOM, getElement } from './utils';\n\ninterface UseIntersectionObserverOptions extends IntersectionObserverInit {\n /**\n * Delay the response update.\n */\n delay?: number;\n /**\n * Trigger the observer only once.\n */\n once?: boolean;\n}\n\nexport function useIntersectionObserver<T extends Element>(\n target: Target<T>,\n options?: UseIntersectionObserverOptions,\n) {\n const { delay = 0, once = false, root = null, rootMargin = '0%', threshold = 0 } = options || {};\n const [value, setValue] = useState<IntersectionObserverEntry>();\n\n const disabled = value?.isIntersecting && once;\n\n const observer = useMemo(() => {\n if (!canUseDOM()) {\n return {};\n }\n\n return new IntersectionObserver(\n ([entry]: IntersectionObserverEntry[]) => {\n if (delay) {\n setTimeout(() => setValue(entry), delay);\n\n return;\n }\n\n setValue(entry);\n },\n { threshold, root, rootMargin },\n );\n }, [delay, root, rootMargin, threshold]);\n\n useEffect(() => {\n if (!canUseDOM() || !(observer instanceof IntersectionObserver) || disabled) {\n return () => undefined;\n }\n\n const element = getElement(target);\n\n if (!element) {\n return () => undefined;\n }\n\n observer.observe(element);\n\n return () => observer.disconnect();\n }, [target, root, rootMargin, disabled, observer]);\n\n return value;\n}\n","import { useEffect, useRef } from 'react';\n\nexport function useInterval(callback: () => void, delayMs: number | null = 100) {\n const savedCallback = useRef(callback);\n\n useEffect(() => {\n savedCallback.current = callback;\n });\n\n useEffect(() => {\n if (delayMs !== null) {\n const interval = setInterval(() => savedCallback.current(), delayMs);\n\n return () => {\n clearInterval(interval);\n };\n }\n\n return undefined;\n }, [delayMs]);\n}\n","import { useEffect, useRef } from 'react';\n\nexport function useLatest<T>(value: T) {\n const ref = useRef(value);\n\n useEffect(() => {\n ref.current = value;\n });\n\n return ref;\n}\n","import { useEffect } from 'react';\n\n/**\n * Run the provided functions on mount and unmount.\n */\nexport function useLifecycleHooks(mount: () => void, unmount: () => void) {\n useEffect(() => {\n mount();\n\n return unmount;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n}\n","import {\n Dispatch,\n SetStateAction,\n useCallback,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport { canUseDOM, noop } from './utils';\n\nexport type UseLocalStorageOptions<TValue> =\n | {\n raw: true;\n }\n | {\n deserializer: (value: string) => TValue;\n raw: false;\n serializer: (value: TValue) => string;\n };\n\nexport type UseLocalStorageResult<TValue> = [\n value: TValue | undefined,\n setValue: Dispatch<SetStateAction<TValue | undefined>>,\n remove: () => void,\n];\n\nfunction useLocalStorageHook<TValue>(\n key: string,\n initialValue?: TValue,\n options?: UseLocalStorageOptions<TValue>,\n): UseLocalStorageResult<TValue> {\n if (!key) {\n throw new Error('useLocalStorage: \"key\" is required');\n }\n\n const deserializer = useMemo(\n () => (options?.raw ? (value: any) => value : (options?.deserializer ?? JSON.parse)),\n [options],\n );\n const serializer = useMemo(\n () => (options?.raw ? String : (options?.serializer ?? JSON.stringify)),\n [options],\n );\n\n const initializer = useRef((k: string) => {\n try {\n const localStorageValue = localStorage.getItem(k);\n\n if (localStorageValue !== null) {\n return deserializer(localStorageValue);\n }\n\n initialValue && localStorage.setItem(k, serializer(initialValue));\n\n return initialValue;\n /* c8 ignore next 6 */\n } catch {\n // If user is in private mode or has storage restriction\n // localStorage can throw. JSON.parse and JSON.stringify\n // can throw, too.\n return initialValue;\n }\n });\n\n const [state, setState] = useState<TValue | undefined>(() => initializer.current(key));\n\n useLayoutEffect(() => setState(initializer.current(key)), [key]);\n\n const set: Dispatch<SetStateAction<TValue | undefined>> = useCallback(\n patch => {\n try {\n const newState = patch instanceof Function ? patch(state) : patch;\n\n if (typeof newState === 'undefined') {\n return;\n }\n\n let value: string;\n\n if (options) {\n if (options.raw) {\n value = typeof newState === 'string' ? newState : JSON.stringify(newState);\n } else if (options?.serializer) {\n value = options.serializer(newState);\n } else {\n value = JSON.stringify(newState);\n }\n } else {\n value = JSON.stringify(newState);\n }\n\n localStorage.setItem(key, value);\n setState(deserializer(value));\n /* c8 ignore next 4 */\n } catch {\n // If user is in private mode or has storage restriction\n // localStorage can throw. Also, JSON.stringify can throw.\n }\n },\n [deserializer, key, options, state],\n );\n\n const remove = useCallback(() => {\n try {\n localStorage.removeItem(key);\n setState(undefined);\n /* c8 ignore next 4 */\n } catch {\n // If user is in private mode or has storage restriction\n // localStorage can throw.\n }\n }, [key, setState]);\n\n return [state, set, remove];\n}\n\nfunction useLocalStorageSSR<TValue>(\n _key: string,\n initialValue?: TValue,\n _options?: UseLocalStorageOptions<TValue>,\n): UseLocalStorageResult<TValue> {\n return [initialValue, noop, noop];\n}\n\nexport const useLocalStorage = canUseDOM() ? useLocalStorageHook : useLocalStorageSSR;\n","export interface UseLocationResult {\n hash: string;\n host: string;\n hostname: string;\n href: string;\n origin: string;\n pathname: string;\n port: string;\n protocol: string;\n query: Record<string, string>;\n search: string;\n}\n\nexport function useLocation(): UseLocationResult {\n const { hash, host, hostname, href, origin, pathname, port, protocol, search } = window.location;\n\n const query = search\n ? search\n .slice(1)\n .split('&')\n .reduce<Record<string, string>>((acc, pair) => {\n const [key, value] = pair.split('=');\n\n if (key && value) {\n acc[key] = value;\n }\n\n return acc;\n }, {})\n : {};\n\n return { hash, host, hostname, href, origin, pathname, port, protocol, query, search };\n}\n","import { useEffect, useState } from 'react';\n\nimport { canUseDOM } from './utils';\n\nexport function useMediaQuery(input: string): boolean {\n const getMatches = (query: string): boolean => {\n if (!canUseDOM()) {\n return false;\n }\n\n return window.matchMedia(query).matches;\n };\n\n const [matches, setMatches] = useState<boolean>(getMatches(input));\n\n function handleChange() {\n setMatches(getMatches(input));\n }\n\n useEffect(() => {\n const matchMedia = window.matchMedia(input);\n\n // Triggered at the first client-side load and if query changes\n handleChange();\n\n try {\n matchMedia.addEventListener('change', handleChange);\n } catch {\n /* c8 ignore next 3 */\n // Safari isn't supporting matchMedia.addEventListener\n matchMedia.addListener(handleChange);\n }\n\n return () => {\n try {\n matchMedia.removeEventListener('change', handleChange);\n } catch {\n /* c8 ignore next 3 */\n // Safari isn't supporting matchMedia.removeEventListener\n matchMedia.removeListener(handleChange);\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [input]);\n\n return matches;\n}\n","import { DependencyList, useMemo, useRef } from 'react';\nimport deepEqual from '@gilbarbara/deep-equal';\n\nimport { validateDependencies } from './utils';\n\nexport function useMemoDeepCompare<T>(factory: () => T, dependencies: DependencyList): T {\n validateDependencies(dependencies, 'useMemoDeepCompare', 'useMemo');\n\n const ref = useRef<DependencyList>(dependencies);\n\n if (!deepEqual(dependencies, ref.current)) {\n ref.current = dependencies;\n }\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n return useMemo(factory, ref.current);\n}\n","import { Ref, RefCallback, RefObject, useCallback } from 'react';\n\nexport function useMergeRefs<T>(...refs: Ref<T>[]): RefCallback<T> {\n return useCallback(\n (value: T) => {\n for (const ref of refs) {\n if (typeof ref === 'function') {\n ref(value);\n } else if (ref && typeof ref === 'object') {\n (ref as RefObject<T>).current = value;\n }\n }\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n refs,\n );\n}\n","import { useEffect } from 'react';\n\nexport function useMount(callback: () => void) {\n useEffect(() => {\n callback();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n}\n","import { useRef } from 'react';\n\nexport function useOnce(callback: () => void): void {\n const hasBeenCalled = useRef(false);\n\n if (hasBeenCalled.current) {\n return;\n }\n\n callback();\n hasBeenCalled.current = true;\n}\n","import { Dispatch, SetStateAction } from 'react';\n\nimport { useEffectDeepCompare } from './useEffectDeepCompare';\nimport { useLocalStorage } from './useLocalStorage';\nimport { useSetState } from './useSetState';\n\nexport type UsePersistentStateResult<T> = [\n state: T,\n setState: Dispatch<SetStateAction<Partial<T>>>,\n remove: () => void,\n];\n\nexport interface UsePersistentStateOptions<TState> {\n /**\n * Check if the saved state keys are different from the initial state and override it if needed.\n * @default false\n */\n overrideDivergentSavedState?: boolean;\n /**\n * Reset properties in the saved state.\n */\n resetProperties?: Partial<TState>;\n}\n\nfunction getState<TState extends object>(\n initialState: TState,\n savedState: TState,\n shouldOverride: boolean,\n restoreProperties?: Partial<TState>,\n): TState {\n if (shouldOverride) {\n const initialStateKeys = Object.keys(initialState);\n const savedStateKeys = savedState ? Object.keys(savedState) : [];\n const restorePropertiesKeys = restoreProperties ? Object.keys(restoreProperties) : [];\n\n if (![...initialStateKeys, ...restorePropertiesKeys].every(k => savedStateKeys.includes(k))) {\n return { ...initialState, ...restoreProperties };\n }\n\n return { ...savedState, ...restoreProperties };\n }\n\n return { ...savedState, ...restoreProperties };\n}\n\nexport function usePersistentState<TState extends object>(\n key: string,\n initialState: TState,\n options?: UsePersistentStateOptions<TState>,\n): UsePersistentStateResult<TState> {\n const { overrideDivergentSavedState = false, resetProperties } = options || {};\n\n const [value, setValue, remove] = useLocalStorage(key, initialState);\n const [state, setState] = useSetState<TState>(\n getState(initialState, value!, overrideDivergentSavedState, resetProperties),\n );\n\n useEffectDeepCompare(() => {\n setValue(state);\n }, [setValue, state]);\n\n return [state, setState, remove];\n}\n","import { useEffect, useRef } from 'react';\n\nexport function useRenderCount(name?: string): number {\n const count = useRef(1);\n\n useEffect(() => {\n count.current += 1;\n });\n\n // eslint-disable-next-line no-console\n console.log(\n `%c${name || 'RenderCount'}: %c${count.current}`,\n 'font-size: 14px; font-weight: bold;',\n 'color: #999; font-size: 14px;',\n );\n\n return count.current;\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { canUseDOM, isString, off, on } from './utils';\n\ninterface UseScriptOptions {\n async?: boolean;\n defer?: boolean;\n id?: string;\n type?: string;\n}\n\nexport type UseScriptResult = [loaded: boolean, error: boolean];\n\nexport function useScript(\n src: string,\n idOrOptions: string | UseScriptOptions = {},\n): [loaded: boolean, error: boolean] {\n const options = useMemo(\n () => (isString(idOrOptions) ? { id: idOrOptions } : idOrOptions),\n [idOrOptions],\n );\n const script = useRef<HTMLScriptElement>(null);\n const [state, setState] = useState({\n loaded: false,\n error: false,\n });\n\n const onLoad = useCallback(() => {\n setState({\n loaded: true,\n error: false,\n });\n }, []);\n\n const onError = useCallback(() => {\n if (script.current) {\n script.current.remove();\n }\n\n setState({\n loaded: false,\n error: true,\n });\n }, []);\n\n useEffect(\n () => {\n if (!canUseDOM() || script.current) {\n return undefined;\n }\n\n const element = document.createElement('script');\n\n element.async = options.async ?? true;\n element.defer = options.defer ?? false;\n element.type = options.type || 'text/javascript';\n element.id = options.id || src;\n element.src = src;\n\n script.current = element;\n\n const { current } = script;\n\n on(current, 'load', onLoad);\n on(current, 'error', onError);\n\n // Add script to document body\n document.body.appendChild(current);\n\n return () => {\n off(current, 'load', onLoad);\n off(current, 'error', onError);\n };\n },\n [onError, onLoad, options, src], // Only re-run effect if script src changes\n );\n\n return [state.loaded, state.error];\n}\n","import { useEffect, useRef, useState } from 'react';\n\nimport { useUnmount } from './useUnmount';\n\nexport function useThrottle<T extends (...arguments_: Array<any>) => void>(\n callback: T,\n delayMs = 500,\n trailing: boolean = false,\n): () => void {\n const [now, setNow] = useState(0);\n const callbackRef = useRef(callback);\n const hasPendingCall = useRef(false);\n const timer = useRef<number>(undefined);\n\n useEffect(() => {\n callbackRef.current = callback;\n }, [callback]);\n\n useEffect(() => {\n if (!now) {\n return;\n }\n\n if (!timer.current) {\n callbackRef.current();\n\n const timerCallback = () => {\n if (hasPendingCall.current) {\n hasPendingCall.current = false;\n\n if (trailing) {\n callbackRef.current();\n }\n\n timer.current = undefined;\n } else {\n timer.current = undefined;\n }\n };\n\n timer.current = window.setTimeout(timerCallback, delayMs);\n } else {\n hasPendingCall.current = true;\n }\n }, [delayMs, now, trailing]);\n\n useUnmount(() => {\n window.clearTimeout(timer.current);\n timer.current = undefined;\n });\n\n return () => setNow(Date.now());\n}\n","import { useEffect } from 'react';\n\nimport { useLatest } from './useLatest';\n\nexport function useUnmount(callback: () => void) {\n const callbackRef = useLatest(callback);\n\n useEffect(() => {\n // eslint-disable-next-line react-hooks/exhaustive-deps\n return () => callbackRef.current();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n}\n","import { useEffect, useRef, useState } from 'react';\n\nimport { useUnmount } from './useUnmount';\n\nexport function useThrottleValue<T>(value: T, delayMs: number): T {\n const [throttledValue, setThrottledValue] = useState<T>(value);\n const hasNextValue = useRef(false);\n const nextValue = useRef<any>(null);\n const timer = useRef<number>(undefined);\n\n useEffect(() => {\n if (!timer.current) {\n setThrottledValue(value);\n\n const timeoutCallback = () => {\n if (hasNextValue.current) {\n hasNextValue.current = false;\n setThrottledValue(nextValue.current);\n timer.current = window.setTimeout(timeoutCallback, delayMs);\n } else {\n timer.current = undefined;\n }\n };\n\n timer.current = window.setTimeout(timeoutCallback, delayMs);\n } else {\n hasNextValue.current = true;\n nextValue.current = value;\n }\n }, [delayMs, value]);\n\n useUnmount(() => {\n window.clearTimeout(timer.current);\n timer.current = undefined;\n });\n\n return throttledValue;\n}\n","import { useCallback, useEffect, useRef } from 'react';\n\nimport { TimerStatus } from './types';\n\nexport type UseTimeoutStatus = TimerStatus;\n\nexport interface UseTimeoutResult {\n cancel: () => void;\n getStatus: () => UseTimeoutStatus;\n reset: () => void;\n}\n\nexport function useTimeout(callback: () => void, delayMs: number = 0): UseTimeoutResult {\n const status = useRef<UseTimeoutStatus>('pending');\n const timeout = useRef<ReturnType<typeof setTimeout>>(null);\n const savedCallback = useRef(callback);\n\n const clear = useCallback(() => {\n status.current = 'cancelled';\n timeout.current && clearTimeout(timeout.current);\n }, []);\n\n const set = useCallback(() => {\n status.current = 'pending';\n timeout.current && clearTimeout(timeout.current);\n\n timeout.current = setTimeout(() => {\n status.current = 'completed';\n savedCallback.current();\n }, delayMs);\n }, [delayMs]);\n\n const getStatus = useCallback(() => status.current, []);\n\n // update ref when function changes\n useEffect(() => {\n savedCallback.current = callback;\n }, [callback]);\n\n // set on mount, clear on unmount\n useEffect(() => {\n set();\n\n return clear;\n }, [set, clear]);\n\n return { cancel: clear, getStatus, reset: set };\n}\n","import { ActionDispatch, AnyActionArg, useCallback, useReducer } from 'react';\n\nexp