(initialValue !== undefined ? initialValue : defaultInitialValue);
return providerFactory({ value: state }, children);
};
diff --git a/src/index.ts b/src/index.ts
index 70e290a965..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,11 +37,12 @@ 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';
export { default as useKeyPressEvent } from './useKeyPressEvent';
+export { default as useLatest } from './useLatest';
export { default as useLifecycles } from './useLifecycles';
export { default as useList } from './useList';
export { default as useLocalStorage } from './useLocalStorage';
@@ -59,7 +60,8 @@ export { default as useMount } from './useMount';
export { default as useMountedState } from './useMountedState';
export { default as useMouse } from './useMouse';
export { default as useMouseHovered } from './useMouseHovered';
-export { default as useNetwork } from './useNetwork';
+export { default as useMouseWheel } from './useMouseWheel';
+export { default as useNetworkState } from './useNetworkState';
export { default as useNumber } from './useNumber';
export { default as useObservable } from './useObservable';
export { default as useOrientation } from './useOrientation';
@@ -73,6 +75,7 @@ export { default as useRaf } from './useRaf';
export { default as useRafLoop } from './useRafLoop';
export { default as useRafState } from './useRafState';
export { default as useSearchParam } from './useSearchParam';
+export { default as useScratch } from './useScratch';
export { default as useScroll } from './useScroll';
export { default as useScrolling } from './useScrolling';
export { default as useSessionStorage } from './useSessionStorage';
@@ -106,7 +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 1348024ca2..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 e20b0b68b6..f30c85caa9 100644
--- a/src/useAsync.ts
+++ b/src/useAsync.ts
@@ -1,13 +1,14 @@
import { DependencyList, useEffect } from 'react';
import useAsyncFn from './useAsyncFn';
+import { FunctionReturningPromise } from './misc/types';
-export { AsyncState, AsyncFn } from './useAsyncFn';
+export { AsyncState, AsyncFnReturn } from './useAsyncFn';
-export default function useAsync(
- fn: (...args: Args | []) => Promise,
+export default function useAsync(
+ fn: T,
deps: DependencyList = []
) {
- const [state, callback] = useAsyncFn(fn, deps, {
+ const [state, callback] = useAsyncFn(fn, deps, {
loading: true,
});
diff --git a/src/useAsyncFn.ts b/src/useAsyncFn.ts
index cd931e5c65..d330ffbadc 100644
--- a/src/useAsyncFn.ts
+++ b/src/useAsyncFn.ts
@@ -1,6 +1,6 @@
-/* eslint-disable */
-import { DependencyList, useCallback, useState, useRef } from 'react';
+import { DependencyList, useCallback, useRef, useState } from 'react';
import useMountedState from './useMountedState';
+import { FunctionReturningPromise, PromiseType } from './misc/types';
export type AsyncState =
| {
@@ -8,6 +8,11 @@ export type AsyncState =
error?: undefined;
value?: undefined;
}
+ | {
+ loading: true;
+ error?: Error | undefined;
+ value?: T;
+ }
| {
loading: false;
error: Error;
@@ -19,38 +24,44 @@ export type AsyncState =
value: T;
};
-export type AsyncFn = [
- AsyncState,
- (...args: Args | []) => Promise
+type StateFromFunctionReturningPromise = AsyncState<
+ PromiseType>
+>;
+
+export type AsyncFnReturn = [
+ StateFromFunctionReturningPromise,
+ T
];
-export default function useAsyncFn(
- fn: (...args: Args | []) => Promise,
+export default function useAsyncFn(
+ fn: T,
deps: DependencyList = [],
- initialState: AsyncState = { loading: false }
-): AsyncFn {
+ initialState: StateFromFunctionReturningPromise = { loading: false }
+): AsyncFnReturn {
const lastCallId = useRef(0);
- const [state, set] = useState>(initialState);
-
const isMounted = useMountedState();
+ const [state, set] = useState>(initialState);
- const callback = useCallback((...args: Args | []) => {
+ const callback = useCallback((...args: Parameters): ReturnType => {
const callId = ++lastCallId.current;
- set({ loading: true });
+
+ if (!state.loading) {
+ set((prevState) => ({ ...prevState, loading: true }));
+ }
return fn(...args).then(
- value => {
+ (value) => {
isMounted() && callId === lastCallId.current && set({ value, loading: false });
return value;
},
- error => {
+ (error) => {
isMounted() && callId === lastCallId.current && set({ error, loading: false });
return error;
}
- );
+ ) as ReturnType;
}, deps);
- return [state, callback];
+ return [state, callback as unknown as T];
}
diff --git a/src/useAsyncRetry.ts b/src/useAsyncRetry.ts
index aa0a43d521..04119d957f 100644
--- a/src/useAsyncRetry.ts
+++ b/src/useAsyncRetry.ts
@@ -1,4 +1,3 @@
-/* eslint-disable */
import { DependencyList, useCallback, useState } from 'react';
import useAsync, { AsyncState } from './useAsync';
@@ -14,13 +13,15 @@ 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;
}
- setAttempt(currentAttempt => currentAttempt + 1);
+ setAttempt((currentAttempt) => currentAttempt + 1);
}, [...deps, stateLoading]);
return { ...state, retry };
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 53083efb71..80d118ac74 100644
--- a/src/useBattery.ts
+++ b/src/useBattery.ts
@@ -1,8 +1,6 @@
-/* eslint-disable */
-import * as React from 'react';
-import { off, on, isDeepEqual } from './util';
-
-const { useState, useEffect } = React;
+import { useEffect, useState } from 'react';
+import { isNavigator, off, on } from './misc/util';
+import isDeepEqual from './misc/isDeepEqual';
export interface BatteryState {
charging: boolean;
@@ -27,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 12794b9bf8..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'];
@@ -13,7 +13,7 @@ const useClickAway = (
savedCallback.current = onClickAway;
}, [onClickAway]);
useEffect(() => {
- const handler = event => {
+ const handler = (event) => {
const { current: el } = ref;
el && !el.contains(event.target) && savedCallback.current(event);
};
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 ca29f714e3..9656d9efd7 100644
--- a/src/useCopyToClipboard.ts
+++ b/src/useCopyToClipboard.ts
@@ -1,4 +1,3 @@
-/* eslint-disable */
import writeText from 'copy-to-clipboard';
import { useCallback } from 'react';
import useMountedState from './useMountedState';
@@ -18,32 +17,49 @@ const useCopyToClipboard = (): [CopyToClipboardState, (value: string) => void] =
noUserInteraction: true,
});
- const copyToClipboard = useCallback(value => {
+ const copyToClipboard = useCallback((value) => {
+ if (!isMounted()) {
+ return;
+ }
+ let noUserInteraction;
+ let normalizedValue;
try {
- if (process.env.NODE_ENV === 'development') {
- if (typeof value !== 'string') {
- console.error(`Cannot copy typeof ${typeof value} to clipboard, must be a string`);
- }
+ // 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`
+ );
+ if (process.env.NODE_ENV === 'development') console.error(error);
+ setState({
+ value,
+ error,
+ noUserInteraction: true,
+ });
+ return;
}
-
- const noUserInteraction = writeText(value);
-
- if (!isMounted()) {
+ // empty strings are also considered invalid
+ else if (value === '') {
+ const error = new Error(`Cannot copy empty string to clipboard.`);
+ if (process.env.NODE_ENV === 'development') console.error(error);
+ setState({
+ value,
+ error,
+ noUserInteraction: true,
+ });
return;
}
+ normalizedValue = value.toString();
+ noUserInteraction = writeText(normalizedValue);
setState({
- value,
+ value: normalizedValue,
error: undefined,
noUserInteraction,
});
} catch (error) {
- if (!isMounted()) {
- return;
- }
setState({
- value: undefined,
+ value: normalizedValue,
error,
- noUserInteraction: true,
+ noUserInteraction,
});
}
}, []);
diff --git a/src/useCounter.ts b/src/useCounter.ts
index 227feca76a..cb4a3b830a 100644
--- a/src/useCounter.ts
+++ b/src/useCounter.ts
@@ -1,24 +1,24 @@
-/* eslint-disable */
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);
@@ -37,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);
@@ -56,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/useCss.ts b/src/useCss.ts
index 2258b67876..5617fe3f97 100644
--- a/src/useCss.ts
+++ b/src/useCss.ts
@@ -2,7 +2,8 @@ import { create, NanoRenderer } from 'nano-css';
import { addon as addonCSSOM, CSSOMAddon } from 'nano-css/addon/cssom';
import { addon as addonVCSSOM, VCSSOMAddon } from 'nano-css/addon/vcssom';
import { cssToTree } from 'nano-css/addon/vcssom/cssToTree';
-import { useLayoutEffect, useMemo } from 'react';
+import { useMemo } from 'react';
+import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
type Nano = NanoRenderer & CSSOMAddon & VCSSOMAddon;
const nano = create() as Nano;
@@ -15,7 +16,7 @@ const useCss = (css: object): string => {
const className = useMemo(() => 'react-use-css-' + (counter++).toString(36), []);
const sheet = useMemo(() => new nano.VSheet(), []);
- useLayoutEffect(() => {
+ useIsomorphicLayoutEffect(() => {
const tree = {};
cssToTree(tree, css, '.' + className, '');
sheet.diff(tree);
diff --git a/src/useCustomCompareEffect.ts b/src/useCustomCompareEffect.ts
index 8f189188e4..0ac38bb1c4 100644
--- a/src/useCustomCompareEffect.ts
+++ b/src/useCustomCompareEffect.ts
@@ -2,12 +2,18 @@ import { DependencyList, EffectCallback, useEffect, useRef } from 'react';
const isPrimitive = (val: any) => val !== Object(val);
-type DepsEqualFnType = (prevDeps: DependencyList, nextDeps: DependencyList) => boolean;
+type DepsEqualFnType = (prevDeps: TDeps, nextDeps: TDeps) => boolean;
-const useCustomCompareEffect = (effect: EffectCallback, deps: DependencyList, depsEqual: DepsEqualFnType) => {
+const useCustomCompareEffect = (
+ effect: EffectCallback,
+ deps: TDeps,
+ depsEqual: DepsEqualFnType
+) => {
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)) {
@@ -17,11 +23,13 @@ const useCustomCompareEffect = (effect: EffectCallback, deps: DependencyList, de
}
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'
+ );
}
}
- const ref = useRef(undefined);
+ const ref = useRef(undefined);
if (!ref.current || !depsEqual(deps, ref.current)) {
ref.current = deps;
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 aa92c8ceca..c4c353a9f1 100644
--- a/src/useDrop.ts
+++ b/src/useDrop.ts
@@ -1,8 +1,5 @@
-/* eslint-disable */
-import * as React from 'react';
-import useMountedState from './useMountedState';
-
-const { useState, useMemo, useCallback, useEffect } = React;
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { noop, off, on } from './misc/util';
export interface DropAreaState {
over: boolean;
@@ -22,14 +19,7 @@ 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 createProcess = (options: DropAreaOptions) => (dataTransfer: DataTransfer, event) => {
const uri = dataTransfer.getData('text/uri-list');
if (uri) {
@@ -42,29 +32,26 @@ const createProcess = (options: DropAreaOptions, mounted: boolean) => (dataTrans
return;
}
- if (dataTransfer.items && dataTransfer.items.length) {
- dataTransfer.items[0].getAsString(text => {
- if (mounted) {
- (options.onText || noop)(text, event);
- }
- });
+ if (event.clipboardData) {
+ const text = event.clipboardData.getData('text');
+ (options.onText || noop)(text, event);
+ return;
}
};
const useDrop = (options: DropAreaOptions = {}, args = []): DropAreaState => {
const { onFiles, onText, onUri } = options;
- const isMounted = useMountedState();
const [over, setOverRaw] = useState(false);
const setOver = useCallback(setOverRaw, []);
- const process = useMemo(() => createProcess(options, isMounted()), [onFiles, onText, onUri]);
+ const process = useMemo(() => createProcess(options), [onFiles, onText, onUri]);
useEffect(() => {
- const onDragOver = event => {
+ const onDragOver = (event) => {
event.preventDefault();
setOver(true);
};
- const onDragEnter = event => {
+ const onDragEnter = (event) => {
event.preventDefault();
setOver(true);
};
@@ -77,32 +64,32 @@ const useDrop = (options: DropAreaOptions = {}, args = []): DropAreaState => {
setOver(false);
};
- const onDrop = event => {
+ const onDrop = (event) => {
event.preventDefault();
setOver(false);
process(event.dataTransfer, event);
};
- const onPaste = event => {
+ const onPaste = (event) => {
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 6f816ec1ee..52ca8fae62 100644
--- a/src/useDropArea.ts
+++ b/src/useDropArea.ts
@@ -1,6 +1,6 @@
-/* eslint-disable */
import { useMemo, useState } from 'react';
import useMountedState from './useMountedState';
+import { noop } from './misc/util';
export interface DropAreaState {
over: boolean;
@@ -20,53 +20,53 @@ 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 => {
+ onDragOver: (event) => {
event.preventDefault();
},
- onDragEnter: event => {
+ onDragEnter: (event) => {
event.preventDefault();
setOver(true);
},
onDragLeave: () => {
setOver(false);
},
- onDrop: event => {
+ onDrop: (event) => {
event.preventDefault();
event.persist();
setOver(false);
process(event.dataTransfer, event);
},
- onPaste: event => {
+ onPaste: (event) => {
event.persist();
process(event.clipboardData, 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 ad86eb3090..c0a1673051 100644
--- a/src/useEvent.ts
+++ b/src/useEvent.ts
@@ -1,20 +1,21 @@
-/* eslint-disable */
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;
@@ -23,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) {
@@ -39,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 6bce030bba..f795bb3b14 100644
--- a/src/useFullscreen.ts
+++ b/src/useFullscreen.ts
@@ -1,20 +1,25 @@
-/* eslint-disable */
-import { RefObject, useLayoutEffect, useState } from 'react';
+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;
+ 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);
- useLayoutEffect(() => {
- if (!on) {
+ useIsomorphicLayoutEffect(() => {
+ 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 8c2c831ccc..5c3f64c770 100644
--- a/src/useGeolocation.ts
+++ b/src/useGeolocation.ts
@@ -1,6 +1,18 @@
-/* eslint-disable */
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;
@@ -11,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 => {
@@ -44,8 +56,8 @@ const useGeolocation = (options?: PositionOptions): GeoLocationSensorState => {
});
}
};
- const onEventError = (error: PositionError) =>
- mounted && setState(oldState => ({ ...oldState, loading: false, error }));
+ const onEventError = (error: IGeolocationPositionError) =>
+ mounted && setState((oldState) => ({ ...oldState, loading: false, error }));
useEffect(() => {
navigator.geolocation.getCurrentPosition(onEvent, onEventError, options);
diff --git a/src/useGetSet.ts b/src/useGetSet.ts
index e4cf860ac4..56a6b99af1 100644
--- a/src/useGetSet.ts
+++ b/src/useGetSet.ts
@@ -1,18 +1,17 @@
-/* eslint-disable */
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 27b1001680..061c178b7e 100644
--- a/src/useGetSetState.ts
+++ b/src/useGetSetState.ts
@@ -1,8 +1,9 @@
-/* eslint-disable */
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
new file mode 100644
index 0000000000..6d936d333a
--- /dev/null
+++ b/src/useHash.ts
@@ -0,0 +1,34 @@
+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
+ */
+export const useHash = () => {
+ const [hash, setHash] = useState(() => window.location.hash);
+
+ const onHashChange = useCallback(() => {
+ setHash(window.location.hash);
+ }, []);
+
+ useLifecycles(
+ () => {
+ on(window, 'hashchange', onHashChange);
+ },
+ () => {
+ off(window, 'hashchange', onHashChange);
+ }
+ );
+
+ const _setHash = useCallback(
+ (newHash: string) => {
+ if (newHash !== hash) {
+ window.location.hash = newHash;
+ }
+ },
+ [hash]
+ );
+
+ return [hash, _setHash] as const;
+};
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 a782c48194..e5eb9a152f 100644
--- a/src/useIdle.ts
+++ b/src/useIdle.ts
@@ -1,12 +1,15 @@
-/* eslint-disable */
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 01dc0292e6..f5b833649d 100644
--- a/src/useIntersection.ts
+++ b/src/useIntersection.ts
@@ -1,11 +1,11 @@
-/* eslint-disable */
import { RefObject, useEffect, useState } from 'react';
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 8affdad06b..03f403bf24 100644
--- a/src/useKey.ts
+++ b/src/useKey.ts
@@ -1,18 +1,17 @@
-/* eslint-disable */
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
@@ -22,11 +21,16 @@ 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);
- const handler: Handler = handlerEvent => {
+ const handler: Handler = (handlerEvent) => {
if (predicate(handlerEvent)) {
return fn(handlerEvent);
}
diff --git a/src/useKeyPress.ts b/src/useKeyPress.ts
index 54b8273aaa..2f48e0fb31 100644
--- a/src/useKeyPress.ts
+++ b/src/useKeyPress.ts
@@ -3,8 +3,8 @@ import useKey, { KeyFilter } from './useKey';
const useKeyPress = (keyFilter: KeyFilter) => {
const [state, set] = useState<[boolean, null | KeyboardEvent]>([false, null]);
- useKey(keyFilter, event => set([true, event]), { event: 'keydown' }, [state]);
- useKey(keyFilter, event => set([false, event]), { event: 'keyup' }, [state]);
+ useKey(keyFilter, (event) => set([true, event]), { event: 'keydown' }, [state]);
+ useKey(keyFilter, (event) => set([false, event]), { event: 'keyup' }, [state]);
return state;
};
diff --git a/src/useKeyboardJs.ts b/src/useKeyboardJs.ts
index 3ae5cc4614..bdf13564e0 100644
--- a/src/useKeyboardJs.ts
+++ b/src/useKeyboardJs.ts
@@ -6,7 +6,7 @@ const useKeyboardJs = (combination: string | string[]) => {
const [keyboardJs, setKeyboardJs] = useState(null);
useMount(() => {
- import('keyboardjs').then(setKeyboardJs);
+ import('keyboardjs').then((k) => setKeyboardJs(k.default || k));
});
useEffect(() => {
@@ -14,9 +14,9 @@ const useKeyboardJs = (combination: string | string[]) => {
return;
}
- const down = event => set([true, event]);
- const up = event => set([false, event]);
- keyboardJs.bind(combination, down, up);
+ const down = (event) => set([true, event]);
+ const up = (event) => set([false, event]);
+ keyboardJs.bind(combination, down, up, true);
return () => {
keyboardJs.unbind(combination, down, up);
diff --git a/src/useLatest.ts b/src/useLatest.ts
new file mode 100644
index 0000000000..cd4230d25c
--- /dev/null
+++ b/src/useLatest.ts
@@ -0,0 +1,9 @@
+import { useRef } from 'react';
+
+const useLatest = (value: T): { readonly current: T } => {
+ const ref = useRef(value);
+ ref.current = value;
+ return ref;
+};
+
+export default useLatest;
diff --git a/src/useLifecycles.ts b/src/useLifecycles.ts
index 77c166223d..45a994298a 100644
--- a/src/useLifecycles.ts
+++ b/src/useLifecycles.ts
@@ -1,4 +1,3 @@
-/* eslint-disable */
import { useEffect } from 'react';
const useLifecycles = (mount, unmount?) => {
diff --git a/src/useList.ts b/src/useList.ts
index c6d94bfe7d..dd592df30d 100644
--- a/src/useList.ts
+++ b/src/useList.ts
@@ -1,13 +1,12 @@
-/* eslint-disable */
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
*/
@@ -63,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();
},
@@ -99,17 +98,17 @@ function useList(initialList: InitialHookState = []): [T[], ListActions<
},
update: (predicate: (a: T, b: T) => boolean, newItem: T) => {
- actions.set((curr: T[]) => curr.map(item => (predicate(item, newItem) ? newItem : item)));
+ actions.set((curr: T[]) => curr.map((item) => (predicate(item, newItem) ? newItem : item)));
},
updateFirst: (predicate: (a: T, b: T) => boolean, newItem: T) => {
- const index = list.current.findIndex(item => predicate(item, newItem));
+ const index = list.current.findIndex((item) => predicate(item, newItem));
index >= 0 && actions.updateAt(index, newItem);
},
upsert: (predicate: (a: T, b: T) => boolean, newItem: T) => {
- const index = list.current.findIndex(item => predicate(item, newItem));
+ const index = list.current.findIndex((item) => predicate(item, newItem));
index >= 0 ? actions.updateAt(index, newItem) : actions.push(newItem);
},
@@ -118,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 698de2ec17..def6ad804a 100644
--- a/src/useLocalStorage.ts
+++ b/src/useLocalStorage.ts
@@ -1,6 +1,5 @@
-/* eslint-disable */
-import { useEffect, useState } from 'react';
-import { isClient } from './util';
+import { Dispatch, SetStateAction, useCallback, useState, useRef, useLayoutEffect } from 'react';
+import { isBrowser, noop } from './misc/util';
type parserOptions =
| {
@@ -16,17 +15,25 @@ const useLocalStorage = (
key: string,
initialValue?: T,
options?: parserOptions
-): [T, React.Dispatch>] => {
- if (!isClient) {
- return [initialValue as T, () => {}];
+): [T | undefined, Dispatch>, () => void] => {
+ if (!isBrowser) {
+ return [initialValue as T, noop, noop];
+ }
+ if (!key) {
+ throw new Error('useLocalStorage key may not be falsy');
}
- // Use provided serializer/deserializer or the default ones
- const serializer = options ? (options.raw ? String : options.serializer) : JSON.stringify;
- const deserializer = options ? (options.raw ? String : options.deserializer) : JSON.parse;
+ const deserializer = options
+ ? options.raw
+ ? (value) => value
+ : options.deserializer
+ : JSON.parse;
- const [state, setState] = useState(() => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const initializer = useRef((key: string) => {
try {
+ const serializer = options ? (options.raw ? String : options.serializer) : JSON.stringify;
+
const localStorageValue = localStorage.getItem(key);
if (localStorageValue !== null) {
return deserializer(localStorageValue);
@@ -42,16 +49,51 @@ const useLocalStorage = (
}
});
- useEffect(() => {
+ // 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;
+ if (typeof newState === 'undefined') return;
+ let value: string;
+
+ if (options)
+ if (options.raw)
+ if (typeof newState === 'string') value = newState;
+ else value = JSON.stringify(newState);
+ else if (options.serializer) value = options.serializer(newState);
+ else value = JSON.stringify(newState);
+ else value = JSON.stringify(newState);
+
+ localStorage.setItem(key, value);
+ setState(deserializer(value));
+ } catch {
+ // If user is in private mode or has storage restriction
+ // localStorage can throw. Also JSON.stringify can throw.
+ }
+ },
+ [key, setState]
+ );
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const remove = useCallback(() => {
try {
- localStorage.setItem(key, serializer(state));
+ localStorage.removeItem(key);
+ setState(undefined);
} catch {
// If user is in private mode or has storage restriction
- // localStorage can throw. Also JSON.stringify can throw.
+ // localStorage can throw.
}
- }, [state]);
+ }, [key, setState]);
- return [state, setState];
+ return [state, set, remove];
};
export default useLocalStorage;
diff --git a/src/useLocation.ts b/src/useLocation.ts
index c593a1d15c..bffdca20b0 100644
--- a/src/useLocation.ts
+++ b/src/useLocation.ts
@@ -1,11 +1,11 @@
-/* eslint-disable */
import { useEffect, useState } from 'react';
-import { isClient, off, on } from './util';
+import { isBrowser, off, on } from './misc/util';
-const patchHistoryMethod = method => {
+const patchHistoryMethod = (method) => {
+ const history = window.history;
const original = history[method];
- history[method] = function(state) {
+ history[method] = function (state) {
const result = original.apply(this, arguments);
const event = new Event(method.toLowerCase());
@@ -17,7 +17,7 @@ const patchHistoryMethod = method => {
};
};
-if (isClient) {
+if (isBrowser) {
patchHistoryMethod('pushState');
patchHistoryMethod('replaceState');
}
@@ -43,9 +43,9 @@ const useLocationServer = (): LocationSensorState => ({
});
const buildState = (trigger: string) => {
- const { state, length } = history;
+ const { state, length } = window.history;
- const { hash, host, hostname, href, origin, pathname, port, protocol, search } = location;
+ const { hash, host, hostname, href, origin, pathname, port, protocol, search } = window.location;
return {
trigger,
@@ -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 b32a92eae1..fd48658917 100644
--- a/src/useLockBodyScroll.ts
+++ b/src/useLockBodyScroll.ts
@@ -1,7 +1,9 @@
-/* eslint-disable */
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') {
@@ -32,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);
@@ -46,49 +48,74 @@ let documentListenerAdded = false;
export default !doc
? function useLockBodyMock(_locked: boolean = true, _elementRef?: RefObject) {}
: function useLockBody(locked: boolean = true, elementRef?: RefObject) {
- elementRef = elementRef || useRef(doc!.body);
+ const bodyRef = useRef(doc!.body);
+ elementRef = elementRef || bodyRef;
- useEffect(() => {
- const body = getClosestBody(elementRef!.current);
- if (!body) {
- return;
+ const lock = (body) => {
+ const bodyInfo = bodies.get(body);
+ if (!bodyInfo) {
+ bodies.set(body, { counter: 1, initialOverflow: body.style.overflow });
+ if (isIosDevice) {
+ if (!documentListenerAdded) {
+ on(document, 'touchmove', preventDefault, { passive: false });
+
+ documentListenerAdded = true;
+ }
+ } else {
+ body.style.overflow = 'hidden';
+ }
+ } else {
+ bodies.set(body, {
+ counter: bodyInfo.counter + 1,
+ initialOverflow: bodyInfo.initialOverflow,
+ });
}
+ };
+ const unlock = (body) => {
const bodyInfo = bodies.get(body);
-
- if (locked) {
- if (!bodyInfo) {
- bodies.set(body, { counter: 1, initialOverflow: body.style.overflow });
+ if (bodyInfo) {
+ if (bodyInfo.counter === 1) {
+ bodies.delete(body);
if (isIosDevice) {
- if (!documentListenerAdded) {
- document.addEventListener('touchmove', preventDefault, { passive: false });
+ body.ontouchmove = null;
- documentListenerAdded = true;
+ if (documentListenerAdded) {
+ off(document, 'touchmove', preventDefault);
+ documentListenerAdded = false;
}
} else {
- body.style.overflow = 'hidden';
+ 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,
+ });
}
+ }
+ };
+
+ useEffect(() => {
+ const body = getClosestBody(elementRef!.current);
+ if (!body) {
+ return;
+ }
+ if (locked) {
+ lock(body);
} else {
- if (bodyInfo) {
- if (bodyInfo.counter === 1) {
- bodies.delete(body);
- if (isIosDevice) {
- body.ontouchmove = null;
-
- if (documentListenerAdded) {
- document.removeEventListener('touchmove', preventDefault);
- documentListenerAdded = false;
- }
- } else {
- body.style.overflow = bodyInfo.initialOverflow;
- }
- } else {
- bodies.set(body, { counter: bodyInfo.counter - 1, initialOverflow: bodyInfo.initialOverflow });
- }
- }
+ unlock(body);
}
}, [locked, elementRef.current]);
+
+ // clean up, on un-mount
+ useEffect(() => {
+ const body = getClosestBody(elementRef!.current);
+ if (!body) {
+ return;
+ }
+ return () => {
+ unlock(body);
+ };
+ }, []);
};
diff --git a/src/useLongPress.ts b/src/useLongPress.ts
index 762b3c944e..a885e091f5 100644
--- a/src/useLongPress.ts
+++ b/src/useLongPress.ts
@@ -1,20 +1,20 @@
-/* eslint-disable */
import { useCallback, useRef } from 'react';
+import { off, on } from './misc/util';
interface Options {
isPreventDefault?: boolean;
delay?: number;
}
-const isTouchEvent = (event: Event): event is TouchEvent => {
- return 'touches' in event;
+const isTouchEvent = (ev: Event): ev is TouchEvent => {
+ return 'touches' in ev;
};
-const preventDefault = (event: Event) => {
- if (!isTouchEvent(event)) return;
+const preventDefault = (ev: Event) => {
+ if (!isTouchEvent(ev)) return;
- if (event.touches.length < 2 && event.preventDefault) {
- event.preventDefault();
+ if (ev.touches.length < 2 && ev.preventDefault) {
+ ev.preventDefault();
}
};
@@ -29,12 +29,12 @@ 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);
},
- [callback, delay]
+ [callback, delay, isPreventDefault]
);
const clear = useCallback(() => {
@@ -42,9 +42,9 @@ const useLongPress = (
timeout.current && clearTimeout(timeout.current);
if (isPreventDefault && target.current) {
- target.current.removeEventListener('touchend', preventDefault);
+ off(target.current, 'touchend', preventDefault);
}
- }, []);
+ }, [isPreventDefault]);
return {
onMouseDown: (e: any) => start(e),
diff --git a/src/useMap.ts b/src/useMap.ts
index 90f2fb01ac..ded74ed239 100644
--- a/src/useMap.ts
+++ b/src/useMap.ts
@@ -1,5 +1,4 @@
-/* eslint-disable */
-import { useState, useMemo, useCallback } from 'react';
+import { useCallback, useMemo, useState } from 'react';
export interface StableActions {
set: (key: K, value: T[K]) => void;
@@ -18,7 +17,7 @@ const useMap = (initialMap: T = {} as T): [T, Actions
const stableActions = useMemo>(
() => ({
set: (key, entry) => {
- set(prevMap => ({
+ set((prevMap) => ({
...prevMap,
[key]: entry,
}));
@@ -26,8 +25,8 @@ const useMap = (initialMap: T = {} as T): [T, Actions
setAll: (newMap: T) => {
set(newMap);
},
- remove: key => {
- set(prevMap => {
+ remove: (key) => {
+ set((prevMap) => {
const { [key]: omit, ...rest } = prevMap;
return rest as T;
});
@@ -38,7 +37,7 @@ const useMap = (initialMap: T = {} as T): [T, Actions
);
const utils = {
- get: useCallback(key => map[key], [map]),
+ get: useCallback((key) => map[key], [map]),
...stableActions,
} as Actions;
diff --git a/src/useMeasure.ts b/src/useMeasure.ts
index 4622355f6a..e14f217f1c 100644
--- a/src/useMeasure.ts
+++ b/src/useMeasure.ts
@@ -1,40 +1,51 @@
-import { useCallback, useState } from 'react';
-import ResizeObserver from 'resize-observer-polyfill';
+import { useMemo, useState } from 'react';
+import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
+import { isBrowser, noop } from './misc/util';
-export type ContentRect = Pick;
+export type UseMeasureRect = Pick<
+ DOMRectReadOnly,
+ 'x' | 'y' | 'top' | 'left' | 'right' | 'bottom' | 'height' | 'width'
+>;
+export type UseMeasureRef = (element: E) => void;
+export type UseMeasureResult = [UseMeasureRef, UseMeasureRect];
-const useMeasure = (): [(instance: T) => void, ContentRect] => {
- const [rect, set] = useState({
- x: 0,
- y: 0,
- width: 0,
- height: 0,
- top: 0,
- left: 0,
- bottom: 0,
- right: 0,
- });
+const defaultState: UseMeasureRect = {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ top: 0,
+ left: 0,
+ bottom: 0,
+ right: 0,
+};
+
+function useMeasure(): UseMeasureResult {
+ const [element, ref] = useState(null);
+ const [rect, setRect] = useState(defaultState);
- const [observer] = useState(
+ const observer = useMemo(
() =>
- new ResizeObserver(entries => {
- const entry = entries[0];
- if (entry) {
- set(entry.contentRect);
+ new (window as any).ResizeObserver((entries) => {
+ if (entries[0]) {
+ const { x, y, width, height, top, left, bottom, right } = entries[0].contentRect;
+ setRect({ x, y, width, height, top, left, bottom, right });
}
- })
+ }),
+ []
);
- const ref = useCallback(
- node => {
+ useIsomorphicLayoutEffect(() => {
+ if (!element) return;
+ observer.observe(element);
+ return () => {
observer.disconnect();
- if (node) {
- observer.observe(node);
- }
- },
- [observer]
- );
+ };
+ }, [element]);
+
return [ref, rect];
-};
+}
-export default useMeasure;
+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 2bc4a82e55..4d5ca794ec 100644
--- a/src/useMeasureDirty.ts
+++ b/src/useMeasureDirty.ts
@@ -1,5 +1,4 @@
-/* eslint-disable */
-import { useState, useEffect, useRef, RefObject } from 'react';
+import { RefObject, useEffect, useRef, useState } from 'react';
import ResizeObserver from 'resize-observer-polyfill';
export interface ContentRect {
@@ -24,14 +23,16 @@ const useMeasureDirty = (ref: RefObject): ContentRect => {
const [observer] = useState(
() =>
- new ResizeObserver(entries => {
+ new ResizeObserver((entries) => {
const entry = entries[0];
if (entry) {
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 d9601274d8..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({});
@@ -12,10 +10,15 @@ const useMediaDevices = () => {
const onChange = () => {
navigator.mediaDevices
.enumerateDevices()
- .then(devices => {
+ .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 cae8085f11..6dbb7a4cb1 100644
--- a/src/useMediatedState.ts
+++ b/src/useMediatedState.ts
@@ -1,4 +1,3 @@
-/* eslint-disable */
import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react';
export interface StateMediator {
@@ -12,9 +11,15 @@ export type UseMediatedStateReturn