(initialValue !== undefined ? initialValue : defaultInitialValue);
return providerFactory({ value: state }, children);
};
diff --git a/src/index.ts b/src/index.ts
index 0742b5fb61..62b69356b7 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,7 +1,7 @@
-export { default as createMemo } from './createMemo';
-export { default as createReducerContext } from './createReducerContext';
-export { default as createReducer } from './createReducer';
-export { default as createStateContext } from './createStateContext';
+export { default as createMemo } from './factory/createMemo';
+export { default as createReducerContext } from './factory/createReducerContext';
+export { default as createReducer } from './factory/createReducer';
+export { default as createStateContext } from './factory/createStateContext';
export { default as useAsync } from './useAsync';
export { default as useAsyncFn } from './useAsyncFn';
export { default as useAsyncRetry } from './useAsyncRetry';
@@ -37,7 +37,7 @@ export { default as useIntersection } from './useIntersection';
export { default as useInterval } from './useInterval';
export { default as useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
export { default as useKey } from './useKey';
-export { default as createBreakpoint } from './createBreakpoint';
+export { default as createBreakpoint } from './factory/createBreakpoint';
// not exported because of peer dependency
// export { default as useKeyboardJs } from './useKeyboardJs';
export { default as useKeyPress } from './useKeyPress';
@@ -61,7 +61,7 @@ export { default as useMountedState } from './useMountedState';
export { default as useMouse } from './useMouse';
export { default as useMouseHovered } from './useMouseHovered';
export { default as useMouseWheel } from './useMouseWheel';
-export { default as useNetwork } from './useNetwork';
+export { default as useNetworkState } from './useNetworkState';
export { default as useNumber } from './useNumber';
export { default as useObservable } from './useObservable';
export { default as useOrientation } from './useOrientation';
@@ -109,8 +109,9 @@ export { useMultiStateValidator } from './useMultiStateValidator';
export { default as useWindowScroll } from './useWindowScroll';
export { default as useWindowSize } from './useWindowSize';
export { default as useMeasure } from './useMeasure';
+export { default as usePinchZoom } from './usePinchZoom';
export { useRendersCount } from './useRendersCount';
export { useFirstMountState } from './useFirstMountState';
export { default as useSet } from './useSet';
-export { createGlobalState } from './createGlobalState';
+export { createGlobalState } from './factory/createGlobalState';
export { useHash } from './useHash';
diff --git a/src/misc/hookState.ts b/src/misc/hookState.ts
new file mode 100644
index 0000000000..8d163e683f
--- /dev/null
+++ b/src/misc/hookState.ts
@@ -0,0 +1,27 @@
+export type IHookStateInitialSetter = () => S;
+export type IHookStateInitAction = S | IHookStateInitialSetter;
+
+export type IHookStateSetter = ((prevState: S) => S) | (() => S);
+export type IHookStateSetAction = S | IHookStateSetter;
+
+export type IHookStateResolvable = S | IHookStateInitialSetter | IHookStateSetter;
+
+export function resolveHookState(nextState: IHookStateInitAction): S;
+export function resolveHookState(
+ nextState: IHookStateSetAction,
+ currentState?: C
+): S;
+export function resolveHookState(
+ nextState: IHookStateResolvable,
+ currentState?: C
+): S;
+export function resolveHookState(
+ nextState: IHookStateResolvable,
+ currentState?: C
+): S {
+ if (typeof nextState === 'function') {
+ return nextState.length ? (nextState as Function)(currentState) : (nextState as Function)();
+ }
+
+ return nextState;
+}
diff --git a/src/misc/isDeepEqual.ts b/src/misc/isDeepEqual.ts
new file mode 100644
index 0000000000..2cff67378a
--- /dev/null
+++ b/src/misc/isDeepEqual.ts
@@ -0,0 +1,3 @@
+import isDeepEqualReact from 'fast-deep-equal/react';
+
+export default isDeepEqualReact;
diff --git a/src/util/parseTimeRanges.ts b/src/misc/parseTimeRanges.ts
similarity index 73%
rename from src/util/parseTimeRanges.ts
rename to src/misc/parseTimeRanges.ts
index 873a2d0241..266479be28 100644
--- a/src/util/parseTimeRanges.ts
+++ b/src/misc/parseTimeRanges.ts
@@ -1,4 +1,4 @@
-const parseTimeRanges = (ranges) => {
+export default function parseTimeRanges(ranges) {
const result: { start: number; end: number }[] = [];
for (let i = 0; i < ranges.length; i++) {
@@ -9,6 +9,4 @@ const parseTimeRanges = (ranges) => {
}
return result;
-};
-
-export default parseTimeRanges;
+}
diff --git a/src/misc/types.ts b/src/misc/types.ts
new file mode 100644
index 0000000000..5c7aa70874
--- /dev/null
+++ b/src/misc/types.ts
@@ -0,0 +1,3 @@
+export type PromiseType> = P extends Promise ? T : never;
+
+export type FunctionReturningPromise = (...args: any[]) => Promise;
diff --git a/src/misc/util.ts b/src/misc/util.ts
new file mode 100644
index 0000000000..34fa42c206
--- /dev/null
+++ b/src/misc/util.ts
@@ -0,0 +1,23 @@
+export const noop = () => {};
+
+export function on(
+ obj: T | null,
+ ...args: Parameters | [string, Function | null, ...any]
+): void {
+ if (obj && obj.addEventListener) {
+ obj.addEventListener(...(args as Parameters));
+ }
+}
+
+export function off(
+ obj: T | null,
+ ...args: Parameters | [string, Function | null, ...any]
+): void {
+ if (obj && obj.removeEventListener) {
+ obj.removeEventListener(...(args as Parameters));
+ }
+}
+
+export const isBrowser = typeof window !== 'undefined';
+
+export const isNavigator = typeof navigator !== 'undefined';
diff --git a/src/useAsync.ts b/src/useAsync.ts
index 887326d9b9..f30c85caa9 100644
--- a/src/useAsync.ts
+++ b/src/useAsync.ts
@@ -1,10 +1,13 @@
import { DependencyList, useEffect } from 'react';
import useAsyncFn from './useAsyncFn';
-import { FnReturningPromise } from './util';
+import { FunctionReturningPromise } from './misc/types';
export { AsyncState, AsyncFnReturn } from './useAsyncFn';
-export default function useAsync(fn: T, deps: DependencyList = []) {
+export default function useAsync(
+ fn: T,
+ deps: DependencyList = []
+) {
const [state, callback] = useAsyncFn(fn, deps, {
loading: true,
});
diff --git a/src/useAsyncFn.ts b/src/useAsyncFn.ts
index a58b4d111a..d330ffbadc 100644
--- a/src/useAsyncFn.ts
+++ b/src/useAsyncFn.ts
@@ -1,6 +1,6 @@
-import { DependencyList, useCallback, useState, useRef } from 'react';
+import { DependencyList, useCallback, useRef, useState } from 'react';
import useMountedState from './useMountedState';
-import { FnReturningPromise, PromiseType } from './util';
+import { FunctionReturningPromise, PromiseType } from './misc/types';
export type AsyncState =
| {
@@ -24,22 +24,30 @@ export type AsyncState =
value: T;
};
-type StateFromFnReturningPromise = AsyncState>>;
+type StateFromFunctionReturningPromise = AsyncState<
+ PromiseType>
+>;
-export type AsyncFnReturn = [StateFromFnReturningPromise, T];
+export type AsyncFnReturn = [
+ StateFromFunctionReturningPromise,
+ T
+];
-export default function useAsyncFn(
+export default function useAsyncFn(
fn: T,
deps: DependencyList = [],
- initialState: StateFromFnReturningPromise = { loading: false }
+ initialState: StateFromFunctionReturningPromise = { loading: false }
): AsyncFnReturn {
const lastCallId = useRef(0);
const isMounted = useMountedState();
- const [state, set] = useState>(initialState);
+ const [state, set] = useState>(initialState);
const callback = useCallback((...args: Parameters): ReturnType => {
const callId = ++lastCallId.current;
- set((prevState) => ({ ...prevState, loading: true }));
+
+ if (!state.loading) {
+ set((prevState) => ({ ...prevState, loading: true }));
+ }
return fn(...args).then(
(value) => {
@@ -55,5 +63,5 @@ export default function useAsyncFn(
) as ReturnType;
}, deps);
- return [state, (callback as unknown) as T];
+ return [state, callback as unknown as T];
}
diff --git a/src/useAsyncRetry.ts b/src/useAsyncRetry.ts
index a51c18b09d..04119d957f 100644
--- a/src/useAsyncRetry.ts
+++ b/src/useAsyncRetry.ts
@@ -13,7 +13,9 @@ const useAsyncRetry = (fn: () => Promise, deps: DependencyList = []) => {
const retry = useCallback(() => {
if (stateLoading) {
if (process.env.NODE_ENV === 'development') {
- console.log('You are calling useAsyncRetry hook retry() method while loading in progress, this is a no-op.');
+ console.log(
+ 'You are calling useAsyncRetry hook retry() method while loading in progress, this is a no-op.'
+ );
}
return;
diff --git a/src/useAudio.ts b/src/useAudio.ts
index b5c0b3e47f..8861d93933 100644
--- a/src/useAudio.ts
+++ b/src/useAudio.ts
@@ -1,5 +1,4 @@
-import createHTMLMediaHook from './util/createHTMLMediaHook';
-
-const useAudio = createHTMLMediaHook('audio');
+import createHTMLMediaHook from './factory/createHTMLMediaHook';
+const useAudio = createHTMLMediaHook('audio');
export default useAudio;
diff --git a/src/useBattery.ts b/src/useBattery.ts
index bdb4a2b026..80d118ac74 100644
--- a/src/useBattery.ts
+++ b/src/useBattery.ts
@@ -1,5 +1,6 @@
-import { useState, useEffect } from 'react';
-import { off, on, isDeepEqual } from './util';
+import { useEffect, useState } from 'react';
+import { isNavigator, off, on } from './misc/util';
+import isDeepEqual from './misc/isDeepEqual';
export interface BatteryState {
charging: boolean;
@@ -24,7 +25,7 @@ type UseBatteryState =
| { isSupported: true; fetched: false } // battery API supported but not fetched yet
| (BatteryState & { isSupported: true; fetched: true }); // battery API supported and fetched
-const nav: NavigatorWithPossibleBattery | undefined = typeof navigator === 'object' ? navigator : undefined;
+const nav: NavigatorWithPossibleBattery | undefined = isNavigator ? navigator : undefined;
const isBatteryApiSupported = nav && typeof nav.getBattery === 'function';
function useBatteryMock(): UseBatteryState {
diff --git a/src/useBeforeUnload.ts b/src/useBeforeUnload.ts
index 83d9410f5d..4d12e77aa6 100644
--- a/src/useBeforeUnload.ts
+++ b/src/useBeforeUnload.ts
@@ -1,4 +1,5 @@
import { useCallback, useEffect } from 'react';
+import { off, on } from './misc/util';
const useBeforeUnload = (enabled: boolean | (() => boolean) = true, message?: string) => {
const handler = useCallback(
@@ -25,9 +26,9 @@ const useBeforeUnload = (enabled: boolean | (() => boolean) = true, message?: st
return;
}
- window.addEventListener('beforeunload', handler);
+ on(window, 'beforeunload', handler);
- return () => window.removeEventListener('beforeunload', handler);
+ return () => off(window, 'beforeunload', handler);
}, [enabled, handler]);
};
diff --git a/src/useClickAway.ts b/src/useClickAway.ts
index 490b918f17..e5367d0f8d 100644
--- a/src/useClickAway.ts
+++ b/src/useClickAway.ts
@@ -1,5 +1,5 @@
import { RefObject, useEffect, useRef } from 'react';
-import { off, on } from './util';
+import { off, on } from './misc/util';
const defaultEvents = ['mousedown', 'touchstart'];
diff --git a/src/useCookie.ts b/src/useCookie.ts
index d81500887c..4bdd6c96de 100644
--- a/src/useCookie.ts
+++ b/src/useCookie.ts
@@ -1,4 +1,4 @@
-import { useState, useCallback } from 'react';
+import { useCallback, useState } from 'react';
import Cookies from 'js-cookie';
const useCookie = (
diff --git a/src/useCopyToClipboard.ts b/src/useCopyToClipboard.ts
index 783ba0effd..9656d9efd7 100644
--- a/src/useCopyToClipboard.ts
+++ b/src/useCopyToClipboard.ts
@@ -26,7 +26,9 @@ const useCopyToClipboard = (): [CopyToClipboardState, (value: string) => void] =
try {
// only strings and numbers casted to strings can be copied to clipboard
if (typeof value !== 'string' && typeof value !== 'number') {
- const error = new Error(`Cannot copy typeof ${typeof value} to clipboard, must be a string`);
+ const error = new Error(
+ `Cannot copy typeof ${typeof value} to clipboard, must be a string`
+ );
if (process.env.NODE_ENV === 'development') console.error(error);
setState({
value,
diff --git a/src/useCounter.ts b/src/useCounter.ts
index 03c0ad36b4..cb4a3b830a 100644
--- a/src/useCounter.ts
+++ b/src/useCounter.ts
@@ -1,23 +1,24 @@
import { useMemo } from 'react';
import useGetSet from './useGetSet';
-import { HookState, InitialHookState, resolveHookState } from './util/resolveHookState';
+import { IHookStateInitAction, IHookStateSetAction, resolveHookState } from './misc/hookState';
export interface CounterActions {
inc: (delta?: number) => void;
dec: (delta?: number) => void;
get: () => number;
- set: (value: HookState) => void;
- reset: (value?: HookState) => void;
+ set: (value: IHookStateSetAction) => void;
+ reset: (value?: IHookStateSetAction) => void;
}
export default function useCounter(
- initialValue: InitialHookState = 0,
+ initialValue: IHookStateInitAction = 0,
max: number | null = null,
min: number | null = null
): [number, CounterActions] {
let init = resolveHookState(initialValue);
- typeof init !== 'number' && console.error('initialValue has to be a number, got ' + typeof initialValue);
+ typeof init !== 'number' &&
+ console.error('initialValue has to be a number, got ' + typeof initialValue);
if (typeof min === 'number') {
init = Math.max(init, min);
@@ -36,7 +37,7 @@ export default function useCounter(
return [
get(),
useMemo(() => {
- const set = (newState: HookState) => {
+ const set = (newState: IHookStateSetAction) => {
const prevState = get();
let rState = resolveHookState(newState, prevState);
@@ -55,31 +56,38 @@ export default function useCounter(
return {
get,
set,
- inc: (delta: HookState = 1) => {
+ inc: (delta: IHookStateSetAction = 1) => {
const rDelta = resolveHookState(delta, get());
if (typeof rDelta !== 'number') {
- console.error('delta has to be a number or function returning a number, got ' + typeof rDelta);
+ console.error(
+ 'delta has to be a number or function returning a number, got ' + typeof rDelta
+ );
}
set((num: number) => num + rDelta);
},
- dec: (delta: HookState = 1) => {
+ dec: (delta: IHookStateSetAction = 1) => {
const rDelta = resolveHookState(delta, get());
if (typeof rDelta !== 'number') {
- console.error('delta has to be a number or function returning a number, got ' + typeof rDelta);
+ console.error(
+ 'delta has to be a number or function returning a number, got ' + typeof rDelta
+ );
}
set((num: number) => num - rDelta);
},
- reset: (value: HookState = init) => {
+ reset: (value: IHookStateSetAction = init) => {
const rValue = resolveHookState(value, get());
if (typeof rValue !== 'number') {
- console.error('value has to be a number or function returning a number, got ' + typeof rValue);
+ console.error(
+ 'value has to be a number or function returning a number, got ' + typeof rValue
+ );
}
+ // eslint-disable-next-line react-hooks/exhaustive-deps
init = rValue;
set(rValue);
},
diff --git a/src/useCustomCompareEffect.ts b/src/useCustomCompareEffect.ts
index b5c7314292..0ac38bb1c4 100644
--- a/src/useCustomCompareEffect.ts
+++ b/src/useCustomCompareEffect.ts
@@ -11,7 +11,9 @@ const useCustomCompareEffect = (
) => {
if (process.env.NODE_ENV !== 'production') {
if (!(deps instanceof Array) || !deps.length) {
- console.warn('`useCustomCompareEffect` should not be used with no dependencies. Use React.useEffect instead.');
+ console.warn(
+ '`useCustomCompareEffect` should not be used with no dependencies. Use React.useEffect instead.'
+ );
}
if (deps.every(isPrimitive)) {
@@ -21,7 +23,9 @@ const useCustomCompareEffect = (
}
if (typeof depsEqual !== 'function') {
- console.warn('`useCustomCompareEffect` should be used with depsEqual callback for comparing deps list');
+ console.warn(
+ '`useCustomCompareEffect` should be used with depsEqual callback for comparing deps list'
+ );
}
}
diff --git a/src/useDebounce.ts b/src/useDebounce.ts
index 3043eecd60..70956d82a1 100644
--- a/src/useDebounce.ts
+++ b/src/useDebounce.ts
@@ -3,7 +3,11 @@ import useTimeoutFn from './useTimeoutFn';
export type UseDebounceReturn = [() => boolean | null, () => void];
-export default function useDebounce(fn: Function, ms: number = 0, deps: DependencyList = []): UseDebounceReturn {
+export default function useDebounce(
+ fn: Function,
+ ms: number = 0,
+ deps: DependencyList = []
+): UseDebounceReturn {
const [isReady, cancel, reset] = useTimeoutFn(fn, ms);
useEffect(reset, deps);
diff --git a/src/useDeepCompareEffect.ts b/src/useDeepCompareEffect.ts
index 1df074cbfe..14c08c4ff0 100644
--- a/src/useDeepCompareEffect.ts
+++ b/src/useDeepCompareEffect.ts
@@ -1,13 +1,15 @@
import { DependencyList, EffectCallback } from 'react';
-import { isDeepEqual } from './util';
import useCustomCompareEffect from './useCustomCompareEffect';
+import isDeepEqual from './misc/isDeepEqual';
const isPrimitive = (val: any) => val !== Object(val);
const useDeepCompareEffect = (effect: EffectCallback, deps: DependencyList) => {
if (process.env.NODE_ENV !== 'production') {
if (!(deps instanceof Array) || !deps.length) {
- console.warn('`useDeepCompareEffect` should not be used with no dependencies. Use React.useEffect instead.');
+ console.warn(
+ '`useDeepCompareEffect` should not be used with no dependencies. Use React.useEffect instead.'
+ );
}
if (deps.every(isPrimitive)) {
diff --git a/src/useDefault.ts b/src/useDefault.ts
index 108ac52289..3234e2c4e9 100644
--- a/src/useDefault.ts
+++ b/src/useDefault.ts
@@ -1,6 +1,9 @@
import { useState } from 'react';
-const useDefault = (defaultValue: TStateType, initialValue: TStateType | (() => TStateType)) => {
+const useDefault = (
+ defaultValue: TStateType,
+ initialValue: TStateType | (() => TStateType)
+) => {
const [value, setValue] = useState(initialValue);
if (value === undefined || value === null) {
diff --git a/src/useDrop.ts b/src/useDrop.ts
index 135594c1cf..c4c353a9f1 100644
--- a/src/useDrop.ts
+++ b/src/useDrop.ts
@@ -1,4 +1,5 @@
-import { useState, useMemo, useCallback, useEffect } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { noop, off, on } from './misc/util';
export interface DropAreaState {
over: boolean;
@@ -18,8 +19,6 @@ export interface DropAreaOptions {
onUri?: (url: string, event?) => void;
}
-const noop = () => {};
-
const createProcess = (options: DropAreaOptions) => (dataTransfer: DataTransfer, event) => {
const uri = dataTransfer.getData('text/uri-list');
@@ -75,22 +74,22 @@ const useDrop = (options: DropAreaOptions = {}, args = []): DropAreaState => {
process(event.clipboardData, event);
};
- document.addEventListener('dragover', onDragOver);
- document.addEventListener('dragenter', onDragEnter);
- document.addEventListener('dragleave', onDragLeave);
- document.addEventListener('dragexit', onDragExit);
- document.addEventListener('drop', onDrop);
+ on(document, 'dragover', onDragOver);
+ on(document, 'dragenter', onDragEnter);
+ on(document, 'dragleave', onDragLeave);
+ on(document, 'dragexit', onDragExit);
+ on(document, 'drop', onDrop);
if (onText) {
- document.addEventListener('paste', onPaste);
+ on(document, 'paste', onPaste);
}
return () => {
- document.removeEventListener('dragover', onDragOver);
- document.removeEventListener('dragenter', onDragEnter);
- document.removeEventListener('dragleave', onDragLeave);
- document.removeEventListener('dragexit', onDragExit);
- document.removeEventListener('drop', onDrop);
- document.removeEventListener('paste', onPaste);
+ off(document, 'dragover', onDragOver);
+ off(document, 'dragenter', onDragEnter);
+ off(document, 'dragleave', onDragLeave);
+ off(document, 'dragexit', onDragExit);
+ off(document, 'drop', onDrop);
+ off(document, 'paste', onPaste);
};
}, [process, ...args]);
diff --git a/src/useDropArea.ts b/src/useDropArea.ts
index ad4cc6ad2e..52ca8fae62 100644
--- a/src/useDropArea.ts
+++ b/src/useDropArea.ts
@@ -1,5 +1,6 @@
import { useMemo, useState } from 'react';
import useMountedState from './useMountedState';
+import { noop } from './misc/util';
export interface DropAreaState {
over: boolean;
@@ -19,34 +20,34 @@ export interface DropAreaOptions {
onUri?: (url: string, event?) => void;
}
-const noop = () => {};
/*
const defaultState: DropAreaState = {
over: false,
};
*/
-const createProcess = (options: DropAreaOptions, mounted: boolean) => (dataTransfer: DataTransfer, event) => {
- const uri = dataTransfer.getData('text/uri-list');
+const createProcess =
+ (options: DropAreaOptions, mounted: boolean) => (dataTransfer: DataTransfer, event) => {
+ const uri = dataTransfer.getData('text/uri-list');
- if (uri) {
- (options.onUri || noop)(uri, event);
- return;
- }
+ if (uri) {
+ (options.onUri || noop)(uri, event);
+ return;
+ }
- if (dataTransfer.files && dataTransfer.files.length) {
- (options.onFiles || noop)(Array.from(dataTransfer.files), event);
- return;
- }
+ if (dataTransfer.files && dataTransfer.files.length) {
+ (options.onFiles || noop)(Array.from(dataTransfer.files), event);
+ return;
+ }
- if (dataTransfer.items && dataTransfer.items.length) {
- dataTransfer.items[0].getAsString((text) => {
- if (mounted) {
- (options.onText || noop)(text, event);
- }
- });
- }
-};
+ if (dataTransfer.items && dataTransfer.items.length) {
+ dataTransfer.items[0].getAsString((text) => {
+ if (mounted) {
+ (options.onText || noop)(text, event);
+ }
+ });
+ }
+ };
const createBond = (process, setOver): DropAreaBond => ({
onDragOver: (event) => {
diff --git a/src/useEnsuredForwardedRef.ts b/src/useEnsuredForwardedRef.ts
index d7451e55a4..ad26fd1a72 100644
--- a/src/useEnsuredForwardedRef.ts
+++ b/src/useEnsuredForwardedRef.ts
@@ -1,16 +1,18 @@
import {
forwardRef,
- useRef,
- useEffect,
- MutableRefObject,
ForwardRefExoticComponent,
+ MutableRefObject,
+ PropsWithChildren,
PropsWithoutRef,
RefAttributes,
RefForwardingComponent,
- PropsWithChildren,
+ useEffect,
+ useRef,
} from 'react';
-export default function useEnsuredForwardedRef(forwardedRef: MutableRefObject): MutableRefObject {
+export default function useEnsuredForwardedRef(
+ forwardedRef: MutableRefObject
+): MutableRefObject {
const ensuredRef = useRef(forwardedRef && forwardedRef.current);
useEffect(() => {
diff --git a/src/useError.ts b/src/useError.ts
index b437901f5f..506549da77 100644
--- a/src/useError.ts
+++ b/src/useError.ts
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback } from 'react';
+import { useCallback, useEffect, useState } from 'react';
const useError = (): ((err: Error) => void) => {
const [error, setError] = useState(null);
diff --git a/src/useEvent.ts b/src/useEvent.ts
index b9c1ec4c36..c0a1673051 100644
--- a/src/useEvent.ts
+++ b/src/useEvent.ts
@@ -1,19 +1,21 @@
import { useEffect } from 'react';
-import { isClient } from './util';
+import { isBrowser, off, on } from './misc/util';
export interface ListenerType1 {
addEventListener(name: string, handler: (event?: any) => void, ...args: any[]);
+
removeEventListener(name: string, handler: (event?: any) => void, ...args: any[]);
}
export interface ListenerType2 {
on(name: string, handler: (event?: any) => void, ...args: any[]);
+
off(name: string, handler: (event?: any) => void, ...args: any[]);
}
export type UseEventTarget = ListenerType1 | ListenerType2;
-const defaultTarget = isClient ? window : null;
+const defaultTarget = isBrowser ? window : null;
const isListenerType1 = (target: any): target is ListenerType1 => {
return !!target.addEventListener;
@@ -22,13 +24,19 @@ const isListenerType2 = (target: any): target is ListenerType2 => {
return !!target.on;
};
-type AddEventListener = T extends ListenerType1 ? T['addEventListener'] : T extends ListenerType2 ? T['on'] : never;
+type AddEventListener = T extends ListenerType1
+ ? T['addEventListener']
+ : T extends ListenerType2
+ ? T['on']
+ : never;
+
+export type UseEventOptions = Parameters>[2];
const useEvent = (
name: Parameters>[0],
handler?: null | undefined | Parameters>[1],
target: null | T | Window = defaultTarget,
- options?: Parameters>[2]
+ options?: UseEventOptions
) => {
useEffect(() => {
if (!handler) {
@@ -38,13 +46,13 @@ const useEvent = (
return;
}
if (isListenerType1(target)) {
- target.addEventListener(name, handler, options);
+ on(target, name, handler, options);
} else if (isListenerType2(target)) {
target.on(name, handler, options);
}
return () => {
if (isListenerType1(target)) {
- target.removeEventListener(name, handler, options);
+ off(target, name, handler, options);
} else if (isListenerType2(target)) {
target.off(name, handler, options);
}
diff --git a/src/useFavicon.ts b/src/useFavicon.ts
index 8f1b80d997..b470cf223e 100644
--- a/src/useFavicon.ts
+++ b/src/useFavicon.ts
@@ -2,7 +2,8 @@ import { useEffect } from 'react';
const useFavicon = (href: string) => {
useEffect(() => {
- const link: HTMLLinkElement = document.querySelector("link[rel*='icon']") || document.createElement('link');
+ const link: HTMLLinkElement =
+ document.querySelector("link[rel*='icon']") || document.createElement('link');
link.type = 'image/x-icon';
link.rel = 'shortcut icon';
link.href = href;
diff --git a/src/useFullscreen.ts b/src/useFullscreen.ts
index ec6e639c17..f795bb3b14 100644
--- a/src/useFullscreen.ts
+++ b/src/useFullscreen.ts
@@ -1,20 +1,25 @@
import { RefObject, useState } from 'react';
import screenfull from 'screenfull';
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
+import { noop, off, on } from './misc/util';
export interface FullScreenOptions {
- video?: RefObject void; webkitExitFullscreen?: () => void }>;
+ video?: RefObject<
+ HTMLVideoElement & { webkitEnterFullscreen?: () => void; webkitExitFullscreen?: () => void }
+ >;
onClose?: (error?: Error) => void;
}
-const noop = () => {};
-
-const useFullscreen = (ref: RefObject, on: boolean, options: FullScreenOptions = {}): boolean => {
+const useFullscreen = (
+ ref: RefObject,
+ enabled: boolean,
+ options: FullScreenOptions = {}
+): boolean => {
const { video, onClose = noop } = options;
- const [isFullscreen, setIsFullscreen] = useState(on);
+ const [isFullscreen, setIsFullscreen] = useState(enabled);
useIsomorphicLayoutEffect(() => {
- if (!on) {
+ if (!enabled) {
return;
}
if (!ref.current) {
@@ -22,7 +27,9 @@ const useFullscreen = (ref: RefObject, on: boolean, options: FullScreen
}
const onWebkitEndFullscreen = () => {
- video!.current!.removeEventListener('webkitendfullscreen', onWebkitEndFullscreen);
+ if (video?.current) {
+ off(video.current, 'webkitendfullscreen', onWebkitEndFullscreen);
+ }
onClose();
};
@@ -47,7 +54,7 @@ const useFullscreen = (ref: RefObject, on: boolean, options: FullScreen
screenfull.on('change', onChange);
} else if (video && video.current && video.current.webkitEnterFullscreen) {
video.current.webkitEnterFullscreen();
- video.current.addEventListener('webkitendfullscreen', onWebkitEndFullscreen);
+ on(video.current, 'webkitendfullscreen', onWebkitEndFullscreen);
setIsFullscreen(true);
} else {
onClose();
@@ -62,11 +69,11 @@ const useFullscreen = (ref: RefObject, on: boolean, options: FullScreen
screenfull.exit();
} catch {}
} else if (video && video.current && video.current.webkitExitFullscreen) {
- video.current.removeEventListener('webkitendfullscreen', onWebkitEndFullscreen);
+ off(video.current, 'webkitendfullscreen', onWebkitEndFullscreen);
video.current.webkitExitFullscreen();
}
};
- }, [on, video, ref]);
+ }, [enabled, video, ref]);
return isFullscreen;
};
diff --git a/src/useGeolocation.ts b/src/useGeolocation.ts
index 27ee387682..5c3f64c770 100644
--- a/src/useGeolocation.ts
+++ b/src/useGeolocation.ts
@@ -1,5 +1,18 @@
import { useEffect, useState } from 'react';
+/**
+ * @desc Made compatible with {GeolocationPositionError} and {PositionError} cause
+ * PositionError been renamed to GeolocationPositionError in typescript 4.1.x and making
+ * own compatible interface is most easiest way to avoid errors.
+ */
+export interface IGeolocationPositionError {
+ readonly code: number;
+ readonly message: string;
+ readonly PERMISSION_DENIED: number;
+ readonly POSITION_UNAVAILABLE: number;
+ readonly TIMEOUT: number;
+}
+
export interface GeoLocationSensorState {
loading: boolean;
accuracy: number | null;
@@ -10,7 +23,7 @@ export interface GeoLocationSensorState {
longitude: number | null;
speed: number | null;
timestamp: number | null;
- error?: Error | PositionError;
+ error?: Error | IGeolocationPositionError;
}
const useGeolocation = (options?: PositionOptions): GeoLocationSensorState => {
@@ -43,7 +56,7 @@ const useGeolocation = (options?: PositionOptions): GeoLocationSensorState => {
});
}
};
- const onEventError = (error: PositionError) =>
+ const onEventError = (error: IGeolocationPositionError) =>
mounted && setState((oldState) => ({ ...oldState, loading: false, error }));
useEffect(() => {
diff --git a/src/useGetSet.ts b/src/useGetSet.ts
index 9cb4e02117..56a6b99af1 100644
--- a/src/useGetSet.ts
+++ b/src/useGetSet.ts
@@ -1,17 +1,17 @@
import { Dispatch, useMemo, useRef } from 'react';
import useUpdate from './useUpdate';
-import { HookState, InitialHookState, resolveHookState } from './util/resolveHookState';
+import { IHookStateInitAction, IHookStateSetAction, resolveHookState } from './misc/hookState';
-export default function useGetSet(initialState: InitialHookState): [() => S, Dispatch>] {
+export default function useGetSet(
+ initialState: IHookStateInitAction
+): [get: () => S, set: Dispatch>] {
const state = useRef(resolveHookState(initialState));
const update = useUpdate();
return useMemo(
() => [
- // get
() => state.current as S,
- // set
- (newState: HookState) => {
+ (newState: IHookStateSetAction) => {
state.current = resolveHookState(newState, state.current);
update();
},
diff --git a/src/useGetSetState.ts b/src/useGetSetState.ts
index 90e70af33e..061c178b7e 100644
--- a/src/useGetSetState.ts
+++ b/src/useGetSetState.ts
@@ -1,7 +1,9 @@
import { useCallback, useRef } from 'react';
import useUpdate from './useUpdate';
-const useGetSetState = (initialState: T = {} as T): [() => T, (patch: Partial) => void] => {
+const useGetSetState = (
+ initialState: T = {} as T
+): [() => T, (patch: Partial) => void] => {
if (process.env.NODE_ENV !== 'production') {
if (typeof initialState !== 'object') {
console.error('useGetSetState initial state must be an object.');
diff --git a/src/useHarmonicIntervalFn.ts b/src/useHarmonicIntervalFn.ts
index d591a32d34..b95a9b6c3b 100644
--- a/src/useHarmonicIntervalFn.ts
+++ b/src/useHarmonicIntervalFn.ts
@@ -1,5 +1,5 @@
import { useEffect, useRef } from 'react';
-import { setHarmonicInterval, clearHarmonicInterval } from 'set-harmonic-interval';
+import { clearHarmonicInterval, setHarmonicInterval } from 'set-harmonic-interval';
const useHarmonicIntervalFn = (fn: Function, delay: number | null = 0) => {
const latestCallback = useRef(() => {});
diff --git a/src/useHash.ts b/src/useHash.ts
index 3d67bcc42a..6d936d333a 100644
--- a/src/useHash.ts
+++ b/src/useHash.ts
@@ -1,5 +1,6 @@
-import { useState, useCallback } from 'react';
+import { useCallback, useState } from 'react';
import useLifecycles from './useLifecycles';
+import { off, on } from './misc/util';
/**
* read and write url hash, response to url hash change
@@ -13,10 +14,10 @@ export const useHash = () => {
useLifecycles(
() => {
- window.addEventListener('hashchange', onHashChange);
+ on(window, 'hashchange', onHashChange);
},
() => {
- window.removeEventListener('hashchange', onHashChange);
+ off(window, 'hashchange', onHashChange);
}
);
diff --git a/src/useHover.ts b/src/useHover.ts
index f7a1c3f5c4..98b5908a8d 100644
--- a/src/useHover.ts
+++ b/src/useHover.ts
@@ -1,9 +1,8 @@
import * as React from 'react';
+import { noop } from './misc/util';
const { useState } = React;
-const noop = () => {};
-
export type Element = ((state: boolean) => React.ReactElement) | React.ReactElement;
const useHover = (element: Element): [React.ReactElement, boolean] => {
diff --git a/src/useHoverDirty.ts b/src/useHoverDirty.ts
index 56274da4b1..4f983a489a 100644
--- a/src/useHoverDirty.ts
+++ b/src/useHoverDirty.ts
@@ -1,4 +1,5 @@
import { RefObject, useEffect, useState } from 'react';
+import { off, on } from './misc/util';
// kudos: https://usehooks.com/
const useHoverDirty = (ref: RefObject, enabled: boolean = true) => {
@@ -15,8 +16,8 @@ const useHoverDirty = (ref: RefObject, enabled: boolean = true) => {
const onMouseOut = () => setValue(false);
if (enabled && ref && ref.current) {
- ref.current.addEventListener('mouseover', onMouseOver);
- ref.current.addEventListener('mouseout', onMouseOut);
+ on(ref.current, 'mouseover', onMouseOver);
+ on(ref.current, 'mouseout', onMouseOut);
}
// fixes react-hooks/exhaustive-deps warning about stale ref elements
@@ -24,8 +25,8 @@ const useHoverDirty = (ref: RefObject, enabled: boolean = true) => {
return () => {
if (enabled && current) {
- current.removeEventListener('mouseover', onMouseOver);
- current.removeEventListener('mouseout', onMouseOut);
+ off(current, 'mouseover', onMouseOver);
+ off(current, 'mouseout', onMouseOut);
}
};
}, [enabled, ref]);
diff --git a/src/useIdle.ts b/src/useIdle.ts
index d03dd48f88..e5eb9a152f 100644
--- a/src/useIdle.ts
+++ b/src/useIdle.ts
@@ -1,11 +1,15 @@
import { useEffect, useState } from 'react';
import { throttle } from 'throttle-debounce';
-import { off, on } from './util';
+import { off, on } from './misc/util';
const defaultEvents = ['mousemove', 'mousedown', 'resize', 'keydown', 'touchstart', 'wheel'];
const oneMinute = 60e3;
-const useIdle = (ms: number = oneMinute, initialState: boolean = false, events: string[] = defaultEvents): boolean => {
+const useIdle = (
+ ms: number = oneMinute,
+ initialState: boolean = false,
+ events: string[] = defaultEvents
+): boolean => {
const [state, setState] = useState(initialState);
useEffect(() => {
diff --git a/src/useIntersection.ts b/src/useIntersection.ts
index 7cf9941dd7..f5b833649d 100644
--- a/src/useIntersection.ts
+++ b/src/useIntersection.ts
@@ -4,7 +4,8 @@ const useIntersection = (
ref: RefObject,
options: IntersectionObserverInit
): IntersectionObserverEntry | null => {
- const [intersectionObserverEntry, setIntersectionObserverEntry] = useState(null);
+ const [intersectionObserverEntry, setIntersectionObserverEntry] =
+ useState(null);
useEffect(() => {
if (ref.current && typeof IntersectionObserver === 'function') {
diff --git a/src/useIsomorphicLayoutEffect.ts b/src/useIsomorphicLayoutEffect.ts
index b285db7843..71f7c3423a 100644
--- a/src/useIsomorphicLayoutEffect.ts
+++ b/src/useIsomorphicLayoutEffect.ts
@@ -1,5 +1,6 @@
import { useEffect, useLayoutEffect } from 'react';
+import { isBrowser } from './misc/util';
-const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
+const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect;
export default useIsomorphicLayoutEffect;
diff --git a/src/useKey.ts b/src/useKey.ts
index dd65b074ac..03f403bf24 100644
--- a/src/useKey.ts
+++ b/src/useKey.ts
@@ -1,17 +1,17 @@
import { DependencyList, useMemo } from 'react';
-import useEvent, { UseEventTarget } from './useEvent';
+import useEvent, { UseEventOptions, UseEventTarget } from './useEvent';
+import { noop } from './misc/util';
export type KeyPredicate = (event: KeyboardEvent) => boolean;
export type KeyFilter = null | undefined | string | ((event: KeyboardEvent) => boolean);
export type Handler = (event: KeyboardEvent) => void;
-export interface UseKeyOptions {
+export interface UseKeyOptions {
event?: 'keydown' | 'keypress' | 'keyup';
- target?: UseEventTarget;
- options?: any;
+ target?: T | null;
+ options?: UseEventOptions;
}
-const noop = () => {};
const createKeyPredicate = (keyFilter: KeyFilter): KeyPredicate =>
typeof keyFilter === 'function'
? keyFilter
@@ -21,7 +21,12 @@ const createKeyPredicate = (keyFilter: KeyFilter): KeyPredicate =>
? () => true
: () => false;
-const useKey = (key: KeyFilter, fn: Handler = noop, opts: UseKeyOptions = {}, deps: DependencyList = [key]) => {
+const useKey = (
+ key: KeyFilter,
+ fn: Handler = noop,
+ opts: UseKeyOptions = {},
+ deps: DependencyList = [key]
+) => {
const { event = 'keydown', target, options } = opts;
const useMemoHandler = useMemo(() => {
const predicate: KeyPredicate = createKeyPredicate(key);
diff --git a/src/useList.ts b/src/useList.ts
index 4ed178c105..dd592df30d 100644
--- a/src/useList.ts
+++ b/src/useList.ts
@@ -1,12 +1,12 @@
import { useMemo, useRef } from 'react';
import useUpdate from './useUpdate';
-import { InitialHookState, ResolvableHookState, resolveHookState } from './util/resolveHookState';
+import { IHookStateInitAction, IHookStateSetAction, resolveHookState } from './misc/hookState';
export interface ListActions {
/**
* @description Set new list instead old one
*/
- set: (newList: ResolvableHookState) => void;
+ set: (newList: IHookStateSetAction) => void;
/**
* @description Add item(s) at the end of list
*/
@@ -62,13 +62,13 @@ export interface ListActions {
reset: () => void;
}
-function useList(initialList: InitialHookState = []): [T[], ListActions] {
+function useList(initialList: IHookStateInitAction = []): [T[], ListActions] {
const list = useRef(resolveHookState(initialList));
const update = useUpdate();
const actions = useMemo>(() => {
const a = {
- set: (newList: ResolvableHookState) => {
+ set: (newList: IHookStateSetAction) => {
list.current = resolveHookState(newList, list.current);
update();
},
@@ -117,7 +117,10 @@ function useList(initialList: InitialHookState = []): [T[], ListActions<
actions.set((curr: T[]) => curr.slice().sort(compareFn));
},
- filter: (callbackFn: (value: T, index: number, array: T[]) => value is S, thisArg?: any) => {
+ filter: (
+ callbackFn: (value: T, index: number, array: T[]) => value is S,
+ thisArg?: any
+ ) => {
actions.set((curr: T[]) => curr.slice().filter(callbackFn, thisArg));
},
diff --git a/src/useLocalStorage.ts b/src/useLocalStorage.ts
index 272069ebb9..def6ad804a 100644
--- a/src/useLocalStorage.ts
+++ b/src/useLocalStorage.ts
@@ -1,5 +1,5 @@
-import { useState, useCallback, Dispatch, SetStateAction } from 'react';
-import { isClient } from './util';
+import { Dispatch, SetStateAction, useCallback, useState, useRef, useLayoutEffect } from 'react';
+import { isBrowser, noop } from './misc/util';
type parserOptions =
| {
@@ -11,24 +11,26 @@ type parserOptions =
deserializer: (value: string) => T;
};
-const noop = () => {};
-
const useLocalStorage = (
key: string,
initialValue?: T,
options?: parserOptions
): [T | undefined, Dispatch>, () => void] => {
- if (!isClient) {
+ if (!isBrowser) {
return [initialValue as T, noop, noop];
}
if (!key) {
throw new Error('useLocalStorage key may not be falsy');
}
- const deserializer = options ? (options.raw ? (value) => value : options.deserializer) : JSON.parse;
+ const deserializer = options
+ ? options.raw
+ ? (value) => value
+ : options.deserializer
+ : JSON.parse;
// eslint-disable-next-line react-hooks/rules-of-hooks
- const [state, setState] = useState(() => {
+ const initializer = useRef((key: string) => {
try {
const serializer = options ? (options.raw ? String : options.serializer) : JSON.stringify;
@@ -47,11 +49,18 @@ const useLocalStorage = (
}
});
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const [state, setState] = useState(() => initializer.current(key));
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ useLayoutEffect(() => setState(initializer.current(key)), [key]);
+
// eslint-disable-next-line react-hooks/rules-of-hooks
const set: Dispatch> = useCallback(
(valOrFunc) => {
try {
- const newState = typeof valOrFunc === 'function' ? (valOrFunc as Function)(state) : valOrFunc;
+ const newState =
+ typeof valOrFunc === 'function' ? (valOrFunc as Function)(state) : valOrFunc;
if (typeof newState === 'undefined') return;
let value: string;
diff --git a/src/useLocation.ts b/src/useLocation.ts
index 44cc123c32..bffdca20b0 100644
--- a/src/useLocation.ts
+++ b/src/useLocation.ts
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
-import { isClient, off, on } from './util';
+import { isBrowser, off, on } from './misc/util';
const patchHistoryMethod = (method) => {
const history = window.history;
@@ -17,7 +17,7 @@ const patchHistoryMethod = (method) => {
};
};
-if (isClient) {
+if (isBrowser) {
patchHistoryMethod('pushState');
patchHistoryMethod('replaceState');
}
@@ -87,4 +87,4 @@ const useLocationBrowser = (): LocationSensorState => {
const hasEventConstructor = typeof Event === 'function';
-export default isClient && hasEventConstructor ? useLocationBrowser : useLocationServer;
+export default isBrowser && hasEventConstructor ? useLocationBrowser : useLocationServer;
diff --git a/src/useLockBodyScroll.ts b/src/useLockBodyScroll.ts
index 6741e80711..fd48658917 100644
--- a/src/useLockBodyScroll.ts
+++ b/src/useLockBodyScroll.ts
@@ -1,6 +1,9 @@
import { RefObject, useEffect, useRef } from 'react';
+import { isBrowser, off, on } from './misc/util';
-export function getClosestBody(el: Element | HTMLElement | HTMLIFrameElement | null): HTMLElement | null {
+export function getClosestBody(
+ el: Element | HTMLElement | HTMLIFrameElement | null
+): HTMLElement | null {
if (!el) {
return null;
} else if (el.tagName === 'BODY') {
@@ -31,7 +34,7 @@ export interface BodyInfoItem {
}
const isIosDevice =
- typeof window !== 'undefined' &&
+ isBrowser &&
window.navigator &&
window.navigator.platform &&
/iP(ad|hone|od)/.test(window.navigator.platform);
@@ -54,7 +57,7 @@ export default !doc
bodies.set(body, { counter: 1, initialOverflow: body.style.overflow });
if (isIosDevice) {
if (!documentListenerAdded) {
- document.addEventListener('touchmove', preventDefault, { passive: false });
+ on(document, 'touchmove', preventDefault, { passive: false });
documentListenerAdded = true;
}
@@ -62,7 +65,10 @@ export default !doc
body.style.overflow = 'hidden';
}
} else {
- bodies.set(body, { counter: bodyInfo.counter + 1, initialOverflow: bodyInfo.initialOverflow });
+ bodies.set(body, {
+ counter: bodyInfo.counter + 1,
+ initialOverflow: bodyInfo.initialOverflow,
+ });
}
};
@@ -75,14 +81,17 @@ export default !doc
body.ontouchmove = null;
if (documentListenerAdded) {
- document.removeEventListener('touchmove', preventDefault);
+ off(document, 'touchmove', preventDefault);
documentListenerAdded = false;
}
} else {
body.style.overflow = bodyInfo.initialOverflow;
}
} else {
- bodies.set(body, { counter: bodyInfo.counter - 1, initialOverflow: bodyInfo.initialOverflow });
+ bodies.set(body, {
+ counter: bodyInfo.counter - 1,
+ initialOverflow: bodyInfo.initialOverflow,
+ });
}
}
};
diff --git a/src/useLongPress.ts b/src/useLongPress.ts
index b6ff738ed3..a885e091f5 100644
--- a/src/useLongPress.ts
+++ b/src/useLongPress.ts
@@ -1,4 +1,5 @@
import { useCallback, useRef } from 'react';
+import { off, on } from './misc/util';
interface Options {
isPreventDefault?: boolean;
@@ -28,7 +29,7 @@ const useLongPress = (
(event: TouchEvent | MouseEvent) => {
// prevent ghost click on mobile devices
if (isPreventDefault && event.target) {
- event.target.addEventListener('touchend', preventDefault, { passive: false });
+ on(event.target, 'touchend', preventDefault, { passive: false });
target.current = event.target;
}
timeout.current = setTimeout(() => callback(event), delay);
@@ -41,7 +42,7 @@ const useLongPress = (
timeout.current && clearTimeout(timeout.current);
if (isPreventDefault && target.current) {
- target.current.removeEventListener('touchend', preventDefault);
+ off(target.current, 'touchend', preventDefault);
}
}, [isPreventDefault]);
diff --git a/src/useMap.ts b/src/useMap.ts
index 0ebcae1ebd..ded74ed239 100644
--- a/src/useMap.ts
+++ b/src/useMap.ts
@@ -1,4 +1,4 @@
-import { useState, useMemo, useCallback } from 'react';
+import { useCallback, useMemo, useState } from 'react';
export interface StableActions {
set: (key: K, value: T[K]) => void;
diff --git a/src/useMeasure.ts b/src/useMeasure.ts
index bdfc34150e..e14f217f1c 100644
--- a/src/useMeasure.ts
+++ b/src/useMeasure.ts
@@ -1,13 +1,13 @@
-import { useState, useMemo } from 'react';
+import { useMemo, useState } from 'react';
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
-import { isClient } from './util';
+import { isBrowser, noop } from './misc/util';
export type UseMeasureRect = Pick<
DOMRectReadOnly,
'x' | 'y' | 'top' | 'left' | 'right' | 'bottom' | 'height' | 'width'
>;
-export type UseMeasureRef = (element: E) => void;
-export type UseMeasureResult = [UseMeasureRef, UseMeasureRect];
+export type UseMeasureRef = (element: E) => void;
+export type UseMeasureResult = [UseMeasureRef, UseMeasureRect];
const defaultState: UseMeasureRect = {
x: 0,
@@ -20,7 +20,7 @@ const defaultState: UseMeasureRect = {
right: 0,
};
-const useMeasure = (): UseMeasureResult => {
+function useMeasure(): UseMeasureResult {
const [element, ref] = useState(null);
const [rect, setRect] = useState(defaultState);
@@ -44,8 +44,8 @@ const useMeasure = (): UseMeasureResult
}, [element]);
return [ref, rect];
-};
-
-const useMeasureMock: typeof useMeasure = () => [() => {}, defaultState];
+}
-export default isClient && !!(window as any).ResizeObserver ? useMeasure : useMeasureMock;
+export default isBrowser && typeof (window as any).ResizeObserver !== 'undefined'
+ ? useMeasure
+ : ((() => [noop, defaultState]) as typeof useMeasure);
diff --git a/src/useMeasureDirty.ts b/src/useMeasureDirty.ts
index d903a59fa2..4d5ca794ec 100644
--- a/src/useMeasureDirty.ts
+++ b/src/useMeasureDirty.ts
@@ -1,4 +1,4 @@
-import { useState, useEffect, useRef, RefObject } from 'react';
+import { RefObject, useEffect, useRef, useState } from 'react';
import ResizeObserver from 'resize-observer-polyfill';
export interface ContentRect {
@@ -30,7 +30,9 @@ const useMeasureDirty = (ref: RefObject): ContentRect => {
cancelAnimationFrame(frame.current);
frame.current = requestAnimationFrame(() => {
- set(entry.contentRect);
+ if (ref.current) {
+ set(entry.contentRect);
+ }
});
}
})
diff --git a/src/useMedia.ts b/src/useMedia.ts
index 053eb21f3c..c0f3d96281 100644
--- a/src/useMedia.ts
+++ b/src/useMedia.ts
@@ -1,8 +1,28 @@
import { useEffect, useState } from 'react';
-import { isClient } from './util';
+import { isBrowser } from './misc/util';
-const useMedia = (query: string, defaultState: boolean = false) => {
- const [state, setState] = useState(isClient ? () => window.matchMedia(query).matches : defaultState);
+const getInitialState = (query: string, defaultState?: boolean) => {
+ // Prevent a React hydration mismatch when a default value is provided by not defaulting to window.matchMedia(query).matches.
+ if (defaultState !== undefined) {
+ return defaultState;
+ }
+
+ if (isBrowser) {
+ return window.matchMedia(query).matches;
+ }
+
+ // A default value has not been provided, and you are rendering on the server, warn of a possible hydration mismatch when defaulting to false.
+ if (process.env.NODE_ENV !== 'production') {
+ console.warn(
+ '`useMedia` When server side rendering, defaultState should be defined to prevent a hydration mismatches.'
+ );
+ }
+
+ return false;
+};
+
+const useMedia = (query: string, defaultState?: boolean) => {
+ const [state, setState] = useState(getInitialState(query, defaultState));
useEffect(() => {
let mounted = true;
@@ -14,12 +34,12 @@ const useMedia = (query: string, defaultState: boolean = false) => {
setState(!!mql.matches);
};
- mql.addListener(onChange);
+ mql.addEventListener('change', onChange);
setState(mql.matches);
return () => {
mounted = false;
- mql.removeListener(onChange);
+ mql.removeEventListener('change', onChange);
};
}, [query]);
diff --git a/src/useMediaDevices.ts b/src/useMediaDevices.ts
index 6a43542e38..50dc53b297 100644
--- a/src/useMediaDevices.ts
+++ b/src/useMediaDevices.ts
@@ -1,7 +1,5 @@
import { useEffect, useState } from 'react';
-import { off, on } from './util';
-
-const noop = () => {};
+import { isNavigator, noop, off, on } from './misc/util';
const useMediaDevices = () => {
const [state, setState] = useState({});
@@ -15,7 +13,12 @@ const useMediaDevices = () => {
.then((devices) => {
if (mounted) {
setState({
- devices: devices.map(({ deviceId, groupId, kind, label }) => ({ deviceId, groupId, kind, label })),
+ devices: devices.map(({ deviceId, groupId, kind, label }) => ({
+ deviceId,
+ groupId,
+ kind,
+ label,
+ })),
});
}
})
@@ -36,4 +39,4 @@ const useMediaDevices = () => {
const useMediaDevicesMock = () => ({});
-export default typeof navigator === 'object' && !!navigator.mediaDevices ? useMediaDevices : useMediaDevicesMock;
+export default isNavigator && !!navigator.mediaDevices ? useMediaDevices : useMediaDevicesMock;
diff --git a/src/useMediatedState.ts b/src/useMediatedState.ts
index 708b2ecf92..6dbb7a4cb1 100644
--- a/src/useMediatedState.ts
+++ b/src/useMediatedState.ts
@@ -11,9 +11,15 @@ export type UseMediatedStateReturn = [S, Dispatch>];
export function useMediatedState(
mediator: StateMediator
): UseMediatedStateReturn;
-export function useMediatedState(mediator: StateMediator, initialState: S): UseMediatedStateReturn;
+export function useMediatedState(
+ mediator: StateMediator,
+ initialState: S
+): UseMediatedStateReturn;
-export function useMediatedState(mediator: StateMediator, initialState?: S): UseMediatedStateReturn {
+export function useMediatedState(
+ mediator: StateMediator,
+ initialState?: S
+): UseMediatedStateReturn {
const mediatorFn = useRef(mediator);
const [state, setMediatedState] = useState(initialState!);
diff --git a/src/useMethods.ts b/src/useMethods.ts
index d265ac3c83..db3cacb618 100644
--- a/src/useMethods.ts
+++ b/src/useMethods.ts
@@ -1,13 +1,11 @@
-import { useMemo, useReducer, Reducer } from 'react';
+import { Reducer, useMemo, useReducer } from 'react';
type Action = {
type: string;
payload?: any;
};
-type CreateMethods = (
- state: T
-) => {
+type CreateMethods = (state: T) => {
[P in keyof M]: (payload?: any) => T;
};
@@ -15,7 +13,10 @@ type WrappedMethods = {
[P in keyof M]: (...payload: any) => void;
};
-const useMethods = (createMethods: CreateMethods, initialState: T): [T, WrappedMethods] => {
+const useMethods = (
+ createMethods: CreateMethods,
+ initialState: T
+): [T, WrappedMethods] => {
const reducer = useMemo>(
() => (reducerState: T, action: Action) => {
return createMethods(reducerState)[action.type](...action.payload);
diff --git a/src/useMotion.ts b/src/useMotion.ts
index e5bb836d27..cb3d867c44 100644
--- a/src/useMotion.ts
+++ b/src/useMotion.ts
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
-import { off, on } from './util';
+import { off, on } from './misc/util';
export interface MotionSensorState {
acceleration: {
diff --git a/src/useMountedState.ts b/src/useMountedState.ts
index b4be5aeb6c..3bdabe8685 100644
--- a/src/useMountedState.ts
+++ b/src/useMountedState.ts
@@ -10,7 +10,7 @@ export default function useMountedState(): () => boolean {
return () => {
mountedRef.current = false;
};
- });
+ }, []);
return get;
}
diff --git a/src/useMouse.ts b/src/useMouse.ts
index 3acb6ccc8d..cbebbba4ef 100644
--- a/src/useMouse.ts
+++ b/src/useMouse.ts
@@ -1,6 +1,7 @@
import { RefObject, useEffect } from 'react';
import useRafState from './useRafState';
+import { off, on } from './misc/util';
export interface State {
docX: number;
@@ -53,10 +54,10 @@ const useMouse = (ref: RefObject): State => {
}
};
- document.addEventListener('mousemove', moveHandler);
+ on(document, 'mousemove', moveHandler);
return () => {
- document.removeEventListener('mousemove', moveHandler);
+ off(document, 'mousemove', moveHandler);
};
}, [ref]);
diff --git a/src/useMouseWheel.ts b/src/useMouseWheel.ts
index 7159e4add1..31638f392f 100644
--- a/src/useMouseWheel.ts
+++ b/src/useMouseWheel.ts
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
+import { off, on } from './misc/util';
export default () => {
const [mouseWheelScrolled, setMouseWheelScrolled] = useState(0);
@@ -6,8 +7,8 @@ export default () => {
const updateScroll = (e: MouseWheelEvent) => {
setMouseWheelScrolled(e.deltaY + mouseWheelScrolled);
};
- window.addEventListener('wheel', updateScroll, false);
- return () => window.removeEventListener('wheel', updateScroll);
+ on(window, 'wheel', updateScroll, false);
+ return () => off(window, 'wheel', updateScroll);
});
return mouseWheelScrolled;
};
diff --git a/src/useMultiStateValidator.ts b/src/useMultiStateValidator.ts
index 1486c5242c..5b01768686 100644
--- a/src/useMultiStateValidator.ts
+++ b/src/useMultiStateValidator.ts
@@ -2,12 +2,16 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { StateValidator, UseStateValidatorReturn, ValidityState } from './useStateValidator';
export type MultiStateValidatorStates = any[] | { [p: string]: any } | { [p: number]: any };
-export type MultiStateValidator = StateValidator;
+export type MultiStateValidator =
+ StateValidator;
-export function useMultiStateValidator(
+export function useMultiStateValidator<
+ V extends ValidityState,
+ S extends MultiStateValidatorStates
+>(
states: S,
validator: MultiStateValidator