Merge pull request #1740 from streamich/refactoring

chore: refactoring and rearrangement.
This commit is contained in:
Anton Zinovyev 2021-01-30 23:37:16 +03:00 committed by GitHub
commit 24ecb0d236
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 2585 additions and 4903 deletions

View File

@ -23,7 +23,7 @@ const Demo = () => {
Or as render-prop:
```jsx
import UseKey from 'react-use/lib/comps/UseKey';
import UseKey from 'react-use/lib/component/UseKey';
<UseKey filter='a' fn={() => alert('"a" key pressed!')} />
```

View File

@ -47,20 +47,19 @@
},
"homepage": "https://github.com/streamich/react-use#readme",
"dependencies": {
"@types/js-cookie": "2.2.6",
"@xobotyi/scrollbar-width": "1.9.5",
"copy-to-clipboard": "^3.2.0",
"@xobotyi/scrollbar-width": "^1.9.5",
"copy-to-clipboard": "^3.3.1",
"fast-deep-equal": "^3.1.3",
"fast-shallow-equal": "^1.0.0",
"js-cookie": "^2.2.1",
"nano-css": "^5.2.1",
"nano-css": "^5.3.1",
"react-universal-interface": "^0.6.2",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^5.0.0",
"screenfull": "^5.1.0",
"set-harmonic-interval": "^1.0.1",
"throttle-debounce": "^2.1.0",
"throttle-debounce": "^3.0.1",
"ts-easing": "^0.2.0",
"tslib": "^2.0.0"
"tslib": "^2.1.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0",
@ -82,9 +81,10 @@
"@storybook/addon-options": "5.3.21",
"@storybook/react": "6.1.15",
"@testing-library/react": "11.2.3",
"@testing-library/react-hooks": "3.7.0",
"@testing-library/react-hooks": "5.0.3",
"@types/jest": "26.0.20",
"@types/react": "16.9.11",
"@types/js-cookie": "2.2.6",
"@types/react": "17.0.0",
"@typescript-eslint/eslint-plugin": "4.14.1",
"@typescript-eslint/parser": "4.14.1",
"babel-core": "6.26.3",
@ -92,13 +92,13 @@
"babel-loader": "8.2.2",
"babel-plugin-dynamic-import-node": "2.3.3",
"eslint": "7.18.0",
"eslint-config-react-app": "5.2.1",
"eslint-config-react-app": "6.0.0",
"eslint-plugin-flowtype": "5.2.0",
"eslint-plugin-import": "2.22.1",
"eslint-plugin-jsx-a11y": "6.4.1",
"eslint-plugin-react": "7.22.0",
"eslint-plugin-react-hooks": "4.2.0",
"fork-ts-checker-webpack-plugin": "5.2.1",
"fork-ts-checker-webpack-plugin": "6.1.0",
"gh-pages": "3.1.0",
"husky": "4.3.8",
"jest": "26.6.3",
@ -108,11 +108,11 @@
"markdown-loader": "6.0.0",
"prettier": "2.2.1",
"raf-stub": "3.0.0",
"react": "16.14.0",
"react-dom": "16.14.0",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-frame-component": "4.1.3",
"react-spring": "8.0.27",
"react-test-renderer": "16.14.0",
"react-test-renderer": "17.0.1",
"rebound": "0.1.0",
"redux-logger": "3.0.6",
"redux-thunk": "2.3.0",
@ -122,7 +122,7 @@
"ts-jest": "26.5.0",
"ts-loader": "8.0.14",
"ts-node": "9.1.1",
"typescript": "3.9.7"
"typescript": "4.1.3"
},
"config": {
"commitizen": {
@ -156,7 +156,7 @@
]
},
"volta": {
"node": "10.23.2",
"node": "10.23.1",
"yarn": "1.22.10"
},
"collective": {

View File

@ -1,5 +1,5 @@
import useKey from '../useKey';
import createRenderProp from '../util/createRenderProp';
import createRenderProp from '../factory/createRenderProp';
const UseKey = createRenderProp(useKey, ({ filter, fn, deps, ...rest }) => [filter, fn, rest, deps]);

View File

@ -1,4 +1,5 @@
import { useEffect, useState, useMemo } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { off, on } from '../misc/util';
const createBreakpoint = (
breakpoints: { [name: string]: number } = { laptopL: 1440, laptop: 1024, tablet: 768 }
@ -10,9 +11,9 @@ const createBreakpoint = (
setScreen(window.innerWidth);
};
setSideScreen();
window.addEventListener('resize', setSideScreen);
on(window, 'resize', setSideScreen);
return () => {
window.removeEventListener('resize', setSideScreen);
off(window, 'resize', setSideScreen);
};
});
const sortedBreakpoints = useMemo(() => Object.entries(breakpoints).sort((a, b) => (a[1] >= b[1] ? 1 : -1)), [

View File

@ -1,6 +1,6 @@
import { useState } from 'react';
import useEffectOnce from './useEffectOnce';
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
import useEffectOnce from '../useEffectOnce';
import useIsomorphicLayoutEffect from '../useIsomorphicLayoutEffect';
export function createGlobalState<S = any>(initialState?: S) {
const store: { state: S | undefined; setState: (state: S) => void; setters: any[] } = {

View File

@ -0,0 +1,235 @@
import * as React from 'react';
import { useEffect, useRef } from 'react';
import useSetState from '../useSetState';
import parseTimeRanges from '../misc/parseTimeRanges';
export interface HTMLMediaProps extends React.AudioHTMLAttributes<any>, React.VideoHTMLAttributes<any> {
src: string;
}
export interface HTMLMediaState {
buffered: any[];
duration: number;
paused: boolean;
muted: boolean;
time: number;
volume: number;
}
export interface HTMLMediaControls {
play: () => Promise<void> | void;
pause: () => void;
mute: () => void;
unmute: () => void;
volume: (volume: number) => void;
seek: (time: number) => void;
}
type createHTMLMediaHookReturn = [
React.ReactElement<HTMLMediaProps>,
HTMLMediaState,
HTMLMediaControls,
{ current: HTMLAudioElement | null }
];
export default function createHTMLMediaHook(tag: 'audio' | 'video') {
return (elOrProps: HTMLMediaProps | React.ReactElement<HTMLMediaProps>): createHTMLMediaHookReturn => {
let element: React.ReactElement<any> | undefined;
let props: HTMLMediaProps;
if (React.isValidElement(elOrProps)) {
element = elOrProps;
props = element.props;
} else {
props = elOrProps as HTMLMediaProps;
}
const [state, setState] = useSetState<HTMLMediaState>({
buffered: [],
time: 0,
duration: 0,
paused: true,
muted: false,
volume: 1,
});
const ref = useRef<HTMLAudioElement | null>(null);
const wrapEvent = (userEvent, proxyEvent?) => {
return (event) => {
try {
proxyEvent && proxyEvent(event);
} finally {
userEvent && userEvent(event);
}
};
};
const onPlay = () => setState({ paused: false });
const onPause = () => setState({ paused: true });
const onVolumeChange = () => {
const el = ref.current;
if (!el) {
return;
}
setState({
muted: el.muted,
volume: el.volume,
});
};
const onDurationChange = () => {
const el = ref.current;
if (!el) {
return;
}
const { duration, buffered } = el;
setState({
duration,
buffered: parseTimeRanges(buffered),
});
};
const onTimeUpdate = () => {
const el = ref.current;
if (!el) {
return;
}
setState({ time: el.currentTime });
};
const onProgress = () => {
const el = ref.current;
if (!el) {
return;
}
setState({ buffered: parseTimeRanges(el.buffered) });
};
if (element) {
element = React.cloneElement(element, {
controls: false,
...props,
ref,
onPlay: wrapEvent(props.onPlay, onPlay),
onPause: wrapEvent(props.onPause, onPause),
onVolumeChange: wrapEvent(props.onVolumeChange, onVolumeChange),
onDurationChange: wrapEvent(props.onDurationChange, onDurationChange),
onTimeUpdate: wrapEvent(props.onTimeUpdate, onTimeUpdate),
onProgress: wrapEvent(props.onProgress, onProgress),
});
} else {
element = React.createElement(tag, {
controls: false,
...props,
ref,
onPlay: wrapEvent(props.onPlay, onPlay),
onPause: wrapEvent(props.onPause, onPause),
onVolumeChange: wrapEvent(props.onVolumeChange, onVolumeChange),
onDurationChange: wrapEvent(props.onDurationChange, onDurationChange),
onTimeUpdate: wrapEvent(props.onTimeUpdate, onTimeUpdate),
onProgress: wrapEvent(props.onProgress, onProgress),
} as any); // TODO: fix this typing.
}
// Some browsers return `Promise` on `.play()` and may throw errors
// if one tries to execute another `.play()` or `.pause()` while that
// promise is resolving. So we prevent that with this lock.
// See: https://bugs.chromium.org/p/chromium/issues/detail?id=593273
let lockPlay: boolean = false;
const controls = {
play: () => {
const el = ref.current;
if (!el) {
return undefined;
}
if (!lockPlay) {
const promise = el.play();
const isPromise = typeof promise === 'object';
if (isPromise) {
lockPlay = true;
const resetLock = () => {
lockPlay = false;
};
promise.then(resetLock, resetLock);
}
return promise;
}
return undefined;
},
pause: () => {
const el = ref.current;
if (el && !lockPlay) {
return el.pause();
}
},
seek: (time: number) => {
const el = ref.current;
if (!el || state.duration === undefined) {
return;
}
time = Math.min(state.duration, Math.max(0, time));
el.currentTime = time;
},
volume: (volume: number) => {
const el = ref.current;
if (!el) {
return;
}
volume = Math.min(1, Math.max(0, volume));
el.volume = volume;
setState({ volume });
},
mute: () => {
const el = ref.current;
if (!el) {
return;
}
el.muted = true;
},
unmute: () => {
const el = ref.current;
if (!el) {
return;
}
el.muted = false;
},
};
useEffect(() => {
const el = ref.current!;
if (!el) {
if (process.env.NODE_ENV !== 'production') {
if (tag === 'audio') {
console.error(
'useAudio() ref to <audio> element is empty at mount. ' +
'It seem you have not rendered the audio element, which it ' +
'returns as the first argument const [audio] = useAudio(...).'
);
} else if (tag === 'video') {
console.error(
'useVideo() ref to <video> element is empty at mount. ' +
'It seem you have not rendered the video element, which it ' +
'returns as the first argument const [video] = useVideo(...).'
);
}
}
return;
}
setState({
volume: el.volume,
muted: el.muted,
paused: el.paused,
});
// Start media, if autoPlay requested.
if (props.autoPlay && el.paused) {
controls.play();
}
}, [props.src]);
return [element, state, controls, ref];
};
}

View File

@ -1,5 +1,5 @@
import { MutableRefObject, useCallback, useRef, useState } from 'react';
import useUpdateEffect from './useUpdateEffect';
import useUpdateEffect from '../useUpdateEffect';
type Dispatch<Action> = (action: Action) => void;

View File

@ -1,4 +1,4 @@
import { createElement, createContext, useContext, useReducer } from 'react';
import { createContext, createElement, useContext, useReducer } from 'react';
const createReducerContext = <R extends React.Reducer<any, any>>(
reducer: R,

View File

@ -1,13 +1,9 @@
const defaultMapPropsToArgs = (props) => [props];
const createRenderProp = (hook, mapPropsToArgs = defaultMapPropsToArgs) => {
const RenderProp = (props) => {
export default function createRenderProp(hook, mapPropsToArgs = defaultMapPropsToArgs) {
return function RenderProp(props) {
const state = hook(...mapPropsToArgs(props));
const { children, render = children } = props;
return render ? render(state) || null : null;
};
return RenderProp;
};
export default createRenderProp;
}

View File

@ -1,4 +1,4 @@
import { createElement, createContext, useContext, useState } from 'react';
import { createContext, createElement, useContext, useState } from 'react';
const createStateContext = <T>(defaultInitialValue: T) => {
const context = createContext<[T, React.Dispatch<React.SetStateAction<T>>] | undefined>(undefined);

View File

@ -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';
@ -112,5 +112,5 @@ export { default as useMeasure } from './useMeasure';
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';

18
src/misc/hookState.ts Normal file
View File

@ -0,0 +1,18 @@
export type IHookStateInitialSetter<S> = () => S;
export type IHookStateInitAction<S> = S | IHookStateInitialSetter<S>;
export type IHookStateSetter<S> = ((prevState: S) => S) | (() => S);
export type IHookStateSetAction<S> = S | IHookStateSetter<S>;
export type IHookStateResolvable<S> = S | IHookStateInitialSetter<S> | IHookStateSetter<S>;
export function resolveHookState<S>(nextState: IHookStateInitAction<S>): S;
export function resolveHookState<S, C extends S>(nextState: IHookStateSetAction<S>, currentState?: C): S;
export function resolveHookState<S, C extends S>(nextState: IHookStateResolvable<S>, currentState?: C): S;
export function resolveHookState<S, C extends S>(nextState: IHookStateResolvable<S>, currentState?: C): S {
if (typeof nextState === 'function') {
return nextState.length ? (nextState as Function)(currentState) : (nextState as Function)();
}
return nextState;
}

3
src/misc/isDeepEqual.ts Normal file
View File

@ -0,0 +1,3 @@
import isDeepEqualReact from 'fast-deep-equal/react';
export default isDeepEqualReact;

View File

@ -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;
}

3
src/misc/types.ts Normal file
View File

@ -0,0 +1,3 @@
export type PromiseType<P extends Promise<any>> = P extends Promise<infer T> ? T : never;
export type FunctionReturningPromise = (...args: any[]) => Promise<any>;

7
src/misc/util.ts Normal file
View File

@ -0,0 +1,7 @@
export const noop = () => {};
export const on = (obj: any, ...args: any[]) => obj.addEventListener(...args);
export const off = (obj: any, ...args: any[]) => obj.removeEventListener(...args);
export const isBrowser = typeof window === 'object';

View File

@ -1,10 +1,10 @@
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<T extends FnReturningPromise>(fn: T, deps: DependencyList = []) {
export default function useAsync<T extends FunctionReturningPromise>(fn: T, deps: DependencyList = []) {
const [state, callback] = useAsyncFn(fn, deps, {
loading: true,
});

View File

@ -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<T> =
| {
@ -24,18 +24,21 @@ export type AsyncState<T> =
value: T;
};
type StateFromFnReturningPromise<T extends FnReturningPromise> = AsyncState<PromiseType<ReturnType<T>>>;
type StateFromFunctionReturningPromise<T extends FunctionReturningPromise> = AsyncState<PromiseType<ReturnType<T>>>;
export type AsyncFnReturn<T extends FnReturningPromise = FnReturningPromise> = [StateFromFnReturningPromise<T>, T];
export type AsyncFnReturn<T extends FunctionReturningPromise = FunctionReturningPromise> = [
StateFromFunctionReturningPromise<T>,
T
];
export default function useAsyncFn<T extends FnReturningPromise>(
export default function useAsyncFn<T extends FunctionReturningPromise>(
fn: T,
deps: DependencyList = [],
initialState: StateFromFnReturningPromise<T> = { loading: false }
initialState: StateFromFunctionReturningPromise<T> = { loading: false }
): AsyncFnReturn<T> {
const lastCallId = useRef(0);
const isMounted = useMountedState();
const [state, set] = useState<StateFromFnReturningPromise<T>>(initialState);
const [state, set] = useState<StateFromFunctionReturningPromise<T>>(initialState);
const callback = useCallback((...args: Parameters<T>): ReturnType<T> => {
const callId = ++lastCallId.current;

View File

@ -1,4 +1,4 @@
import createHTMLMediaHook from './util/createHTMLMediaHook';
import createHTMLMediaHook from './factory/createHTMLMediaHook';
const useAudio = createHTMLMediaHook('audio');

View File

@ -1,5 +1,6 @@
import { useState, useEffect } from 'react';
import { off, on, isDeepEqual } from './util';
import { useEffect, useState } from 'react';
import { off, on } from './misc/util';
import isDeepEqual from './misc/isDeepEqual';
export interface BatteryState {
charging: boolean;

View File

@ -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]);
};

View File

@ -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'];

View File

@ -1,4 +1,4 @@
import { useState, useCallback } from 'react';
import { useCallback, useState } from 'react';
import Cookies from 'js-cookie';
const useCookie = (

View File

@ -1,17 +1,17 @@
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<number>) => void;
reset: (value?: HookState<number>) => void;
set: (value: IHookStateSetAction<number>) => void;
reset: (value?: IHookStateSetAction<number>) => void;
}
export default function useCounter(
initialValue: InitialHookState<number> = 0,
initialValue: IHookStateInitAction<number> = 0,
max: number | null = null,
min: number | null = null
): [number, CounterActions] {
@ -36,7 +36,7 @@ export default function useCounter(
return [
get(),
useMemo(() => {
const set = (newState: HookState<number>) => {
const set = (newState: IHookStateSetAction<number>) => {
const prevState = get();
let rState = resolveHookState(newState, prevState);
@ -55,7 +55,7 @@ export default function useCounter(
return {
get,
set,
inc: (delta: HookState<number> = 1) => {
inc: (delta: IHookStateSetAction<number> = 1) => {
const rDelta = resolveHookState(delta, get());
if (typeof rDelta !== 'number') {
@ -64,7 +64,7 @@ export default function useCounter(
set((num: number) => num + rDelta);
},
dec: (delta: HookState<number> = 1) => {
dec: (delta: IHookStateSetAction<number> = 1) => {
const rDelta = resolveHookState(delta, get());
if (typeof rDelta !== 'number') {
@ -73,13 +73,14 @@ export default function useCounter(
set((num: number) => num - rDelta);
},
reset: (value: HookState<number> = init) => {
reset: (value: IHookStateSetAction<number> = 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);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
init = rValue;
set(rValue);
},

View File

@ -1,6 +1,6 @@
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);

View File

@ -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]);

View File

@ -1,5 +1,6 @@
import { useMemo, useState } from 'react';
import useMountedState from './useMountedState';
import { noop } from './misc/util';
export interface DropAreaState {
over: boolean;
@ -19,7 +20,6 @@ export interface DropAreaOptions {
onUri?: (url: string, event?) => void;
}
const noop = () => {};
/*
const defaultState: DropAreaState = {
over: false,

View File

@ -1,13 +1,13 @@
import {
forwardRef,
useRef,
useEffect,
MutableRefObject,
ForwardRefExoticComponent,
MutableRefObject,
PropsWithChildren,
PropsWithoutRef,
RefAttributes,
RefForwardingComponent,
PropsWithChildren,
useEffect,
useRef,
} from 'react';
export default function useEnsuredForwardedRef<T>(forwardedRef: MutableRefObject<T>): MutableRefObject<T> {

View File

@ -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<Error | null>(null);

View File

@ -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;
@ -38,13 +40,13 @@ const useEvent = <T extends UseEventTarget>(
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);
}

View File

@ -1,20 +1,19 @@
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<HTMLVideoElement & { webkitEnterFullscreen?: () => void; webkitExitFullscreen?: () => void }>;
onClose?: (error?: Error) => void;
}
const noop = () => {};
const useFullscreen = (ref: RefObject<Element>, on: boolean, options: FullScreenOptions = {}): boolean => {
const useFullscreen = (ref: RefObject<Element>, 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 +21,9 @@ const useFullscreen = (ref: RefObject<Element>, on: boolean, options: FullScreen
}
const onWebkitEndFullscreen = () => {
video!.current!.removeEventListener('webkitendfullscreen', onWebkitEndFullscreen);
if (video?.current) {
off(video.current, 'webkitendfullscreen', onWebkitEndFullscreen);
}
onClose();
};
@ -47,7 +48,7 @@ const useFullscreen = (ref: RefObject<Element>, 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 +63,11 @@ const useFullscreen = (ref: RefObject<Element>, 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;
};

View File

@ -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<S>(initialState: InitialHookState<S>): [() => S, Dispatch<HookState<S>>] {
export default function useGetSet<S>(
initialState: IHookStateInitAction<S>
): [get: () => S, set: Dispatch<IHookStateSetAction<S>>] {
const state = useRef(resolveHookState(initialState));
const update = useUpdate();
return useMemo(
() => [
// get
() => state.current as S,
// set
(newState: HookState<S>) => {
(newState: IHookStateSetAction<S>) => {
state.current = resolveHookState(newState, state.current);
update();
},

View File

@ -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<Function>(() => {});

View File

@ -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);
}
);

View File

@ -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<any>) | React.ReactElement<any>;
const useHover = (element: Element): [React.ReactElement<any>, boolean] => {

View File

@ -1,4 +1,5 @@
import { RefObject, useEffect, useState } from 'react';
import { off, on } from './misc/util';
// kudos: https://usehooks.com/
const useHoverDirty = (ref: RefObject<Element>, enabled: boolean = true) => {
@ -15,8 +16,8 @@ const useHoverDirty = (ref: RefObject<Element>, 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<Element>, 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]);

View File

@ -1,6 +1,6 @@
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;

View File

@ -1,5 +1,6 @@
import { DependencyList, useMemo } from 'react';
import useEvent, { UseEventTarget } from './useEvent';
import { noop } from './misc/util';
export type KeyPredicate = (event: KeyboardEvent) => boolean;
export type KeyFilter = null | undefined | string | ((event: KeyboardEvent) => boolean);
@ -11,7 +12,6 @@ export interface UseKeyOptions {
options?: any;
}
const noop = () => {};
const createKeyPredicate = (keyFilter: KeyFilter): KeyPredicate =>
typeof keyFilter === 'function'
? keyFilter

View File

@ -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<T> {
/**
* @description Set new list instead old one
*/
set: (newList: ResolvableHookState<T[]>) => void;
set: (newList: IHookStateSetAction<T[]>) => void;
/**
* @description Add item(s) at the end of list
*/
@ -62,13 +62,13 @@ export interface ListActions<T> {
reset: () => void;
}
function useList<T>(initialList: InitialHookState<T[]> = []): [T[], ListActions<T>] {
function useList<T>(initialList: IHookStateInitAction<T[]> = []): [T[], ListActions<T>] {
const list = useRef(resolveHookState(initialList));
const update = useUpdate();
const actions = useMemo<ListActions<T>>(() => {
const a = {
set: (newList: ResolvableHookState<T[]>) => {
set: (newList: IHookStateSetAction<T[]>) => {
list.current = resolveHookState(newList, list.current);
update();
},

View File

@ -1,5 +1,5 @@
import { useState, useCallback, Dispatch, SetStateAction } from 'react';
import { isClient } from './util';
import { Dispatch, SetStateAction, useCallback, useState } from 'react';
import { isBrowser, noop } from './misc/util';
type parserOptions<T> =
| {
@ -11,14 +11,12 @@ type parserOptions<T> =
deserializer: (value: string) => T;
};
const noop = () => {};
const useLocalStorage = <T>(
key: string,
initialValue?: T,
options?: parserOptions<T>
): [T | undefined, Dispatch<SetStateAction<T | undefined>>, () => void] => {
if (!isClient) {
if (!isBrowser) {
return [initialValue as T, noop, noop];
}
if (!key) {

View File

@ -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;

View File

@ -1,4 +1,5 @@
import { RefObject, useEffect, useRef } from 'react';
import { isBrowser, off, on } from './misc/util';
export function getClosestBody(el: Element | HTMLElement | HTMLIFrameElement | null): HTMLElement | null {
if (!el) {
@ -31,10 +32,7 @@ export interface BodyInfoItem {
}
const isIosDevice =
typeof window !== 'undefined' &&
window.navigator &&
window.navigator.platform &&
/iP(ad|hone|od)/.test(window.navigator.platform);
isBrowser && window.navigator && window.navigator.platform && /iP(ad|hone|od)/.test(window.navigator.platform);
const bodies: Map<HTMLElement, BodyInfoItem> = new Map();
@ -54,7 +52,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;
}
@ -75,7 +73,7 @@ export default !doc
body.ontouchmove = null;
if (documentListenerAdded) {
document.removeEventListener('touchmove', preventDefault);
off(document, 'touchmove', preventDefault);
documentListenerAdded = false;
}
} else {

View File

@ -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]);

View File

@ -1,4 +1,4 @@
import { useState, useMemo, useCallback } from 'react';
import { useCallback, useMemo, useState } from 'react';
export interface StableActions<T extends object> {
set: <K extends keyof T>(key: K, value: T[K]) => void;

View File

@ -1,6 +1,6 @@
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,
@ -20,7 +20,7 @@ const defaultState: UseMeasureRect = {
right: 0,
};
const useMeasure = <E extends HTMLElement = HTMLElement>(): UseMeasureResult<E> => {
function useMeasure<E extends HTMLElement = HTMLElement>(): UseMeasureResult<E> {
const [element, ref] = useState<E | null>(null);
const [rect, setRect] = useState<UseMeasureRect>(defaultState);
@ -44,8 +44,8 @@ const useMeasure = <E extends HTMLElement = HTMLElement>(): UseMeasureResult<E>
}, [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];

View File

@ -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 {

View File

@ -1,8 +1,8 @@
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 [state, setState] = useState(isBrowser ? () => window.matchMedia(query).matches : defaultState);
useEffect(() => {
let mounted = true;

View File

@ -1,7 +1,5 @@
import { useEffect, useState } from 'react';
import { off, on } from './util';
const noop = () => {};
import { noop, off, on } from './misc/util';
const useMediaDevices = () => {
const [state, setState] = useState({});

View File

@ -1,4 +1,4 @@
import { useMemo, useReducer, Reducer } from 'react';
import { Reducer, useMemo, useReducer } from 'react';
type Action = {
type: string;

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { off, on } from './util';
import { off, on } from './misc/util';
export interface MotionSensorState {
acceleration: {

View File

@ -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<Element>): State => {
}
};
document.addEventListener('mousemove', moveHandler);
on(document, 'mousemove', moveHandler);
return () => {
document.removeEventListener('mousemove', moveHandler);
off(document, 'mousemove', moveHandler);
};
}, [ref]);

View File

@ -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;
};

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { off, on } from './util';
import { off, on } from './misc/util';
export interface NetworkState {
online?: boolean;

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { off, on } from './util';
import { off, on } from './misc/util';
export interface OrientationState {
angle: number;

View File

@ -1,4 +1,5 @@
import { useEffect } from 'react';
import { off, on } from './misc/util';
const usePageLeave = (onPageLeave, args = []) => {
useEffect(() => {
@ -14,9 +15,9 @@ const usePageLeave = (onPageLeave, args = []) => {
}
};
document.addEventListener('mouseout', handler);
on(document, 'mouseout', handler);
return () => {
document.removeEventListener('mouseout', handler);
off(document, 'mouseout', handler);
};
}, args);
};

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { off, on } from './util';
import { noop, off, on } from './misc/util';
type PermissionDesc =
| PermissionDescriptor
@ -9,8 +9,6 @@ type PermissionDesc =
type State = PermissionState | '';
const noop = () => {};
const usePermission = (permissionDesc: PermissionDesc): State => {
let mounted = true;
let permissionStatus: PermissionStatus | null = null;

View File

@ -1,4 +1,4 @@
import { useRef, useState, useCallback, Dispatch, SetStateAction } from 'react';
import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react';
import useUnmount from './useUnmount';

View File

@ -1,8 +1,7 @@
import { useState, useEffect, useRef, FC, cloneElement } from 'react';
import { cloneElement, FC, useEffect, useRef, useState } from 'react';
import { render } from 'react-universal-interface';
import useLatest from './useLatest';
const noop = () => {};
import { noop, off, on } from './misc/util';
export interface ScratchSensorParams {
disabled?: boolean;
@ -81,10 +80,10 @@ const useScratch = (params: ScratchSensorParams = {}): [(el: HTMLElement | null)
refState.current = { ...refState.current, isScratching: false };
(paramsRef.current.onScratchEnd || noop)(refState.current);
setState({ isScratching: false });
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('mouseup', onMouseUp);
window.removeEventListener('touchend', onTouchEnd);
off(window, 'mousemove', onMouseMove);
off(window, 'touchmove', onTouchMove);
off(window, 'mouseup', onMouseUp);
off(window, 'touchend', onTouchEnd);
};
onMouseUp = stopScratching;
@ -116,10 +115,10 @@ const useScratch = (params: ScratchSensorParams = {}): [(el: HTMLElement | null)
refState.current = newState;
(paramsRef.current.onScratchStart || noop)(newState);
setState(newState);
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('touchmove', onTouchMove);
window.addEventListener('mouseup', onMouseUp);
window.addEventListener('touchend', onTouchEnd);
on(window, 'mousemove', onMouseMove);
on(window, 'touchmove', onTouchMove);
on(window, 'mouseup', onMouseUp);
on(window, 'touchend', onTouchEnd);
};
const onMouseDown = (event) => {
@ -132,16 +131,16 @@ const useScratch = (params: ScratchSensorParams = {}): [(el: HTMLElement | null)
startScratching(event.changedTouches[0].pageX, event.changedTouches[0].pageY);
};
el.addEventListener('mousedown', onMouseDown);
el.addEventListener('touchstart', onTouchStart);
on(el, 'mousedown', onMouseDown);
on(el, 'touchstart', onTouchStart);
return () => {
el.removeEventListener('mousedown', onMouseDown);
el.removeEventListener('touchstart', onTouchStart);
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('mouseup', onMouseUp);
window.removeEventListener('touchend', onTouchEnd);
off(el, 'mousedown', onMouseDown);
off(el, 'touchstart', onTouchStart);
off(window, 'mousemove', onMouseMove);
off(window, 'touchmove', onTouchMove);
off(window, 'mouseup', onMouseUp);
off(window, 'touchend', onTouchEnd);
if (refAnimationFrame.current) cancelAnimationFrame(refAnimationFrame.current);
refAnimationFrame.current = null;

View File

@ -1,6 +1,7 @@
import { RefObject, useEffect } from 'react';
import useRafState from './useRafState';
import { off, on } from './misc/util';
export interface State {
x: number;
@ -30,7 +31,7 @@ const useScroll = (ref: RefObject<HTMLElement>): State => {
};
if (ref.current) {
ref.current.addEventListener('scroll', handler, {
on(ref.current, 'scroll', handler, {
capture: false,
passive: true,
});
@ -38,7 +39,7 @@ const useScroll = (ref: RefObject<HTMLElement>): State => {
return () => {
if (ref.current) {
ref.current.removeEventListener('scroll', handler);
off(ref.current, 'scroll', handler);
}
};
}, [ref]);

View File

@ -1,4 +1,5 @@
import { RefObject, useEffect, useState } from 'react';
import { off, on } from './misc/util';
const useScrolling = (ref: RefObject<HTMLElement>): boolean => {
const [scrolling, setScrolling] = useState<boolean>(false);
@ -17,10 +18,10 @@ const useScrolling = (ref: RefObject<HTMLElement>): boolean => {
scrollingTimeout = setTimeout(() => handleScrollEnd(), 150);
};
ref.current.addEventListener('scroll', handleScroll, false);
on(ref.current, 'scroll', handleScroll, false);
return () => {
if (ref.current) {
ref.current.removeEventListener('scroll', handleScroll, false);
off(ref.current, 'scroll', handleScroll, false);
}
};
}

View File

@ -1,4 +1,5 @@
import { useState, useEffect } from 'react';
import { useEffect, useState } from 'react';
import { isBrowser, off, on } from './misc/util';
const getValue = (search: string, param: string) => new URLSearchParams(search).get(param);
@ -13,14 +14,14 @@ const useSearchParam: UseQueryParam = (param) => {
setValue(getValue(location.search, param));
};
window.addEventListener('popstate', onChange);
window.addEventListener('pushstate', onChange);
window.addEventListener('replacestate', onChange);
on(window, 'popstate', onChange);
on(window, 'pushstate', onChange);
on(window, 'replacestate', onChange);
return () => {
window.removeEventListener('popstate', onChange);
window.removeEventListener('pushstate', onChange);
window.removeEventListener('replacestate', onChange);
off(window, 'popstate', onChange);
off(window, 'pushstate', onChange);
off(window, 'replacestate', onChange);
};
}, []);
@ -29,4 +30,4 @@ const useSearchParam: UseQueryParam = (param) => {
const useSearchParamServer = () => null;
export default typeof window === 'object' ? useSearchParam : useSearchParamServer;
export default isBrowser ? useSearchParam : useSearchParamServer;

View File

@ -1,8 +1,8 @@
import { useEffect, useState } from 'react';
import { isClient } from './util';
import { isBrowser } from './misc/util';
const useSessionStorage = <T>(key: string, initialValue?: T, raw?: boolean): [T, (value: T) => void] => {
if (!isClient) {
if (!isBrowser) {
return [initialValue as T, () => {}];
}

View File

@ -1,4 +1,4 @@
import { useState, useMemo, useCallback } from 'react';
import { useCallback, useMemo, useState } from 'react';
export interface StableActions<K> {
add: (key: K) => void;

View File

@ -1,5 +1,5 @@
import * as React from 'react';
import { isClient } from './util';
import { isBrowser, off, on } from './misc/util';
const { useState, useEffect, useRef } = React;
@ -16,7 +16,7 @@ const useSize = (
element: Element,
{ width = Infinity, height = Infinity }: Partial<State> = {}
): [React.ReactElement<any>, State] => {
if (!isClient) {
if (!isBrowser) {
return [typeof element === 'function' ? element({ width, height }) : element, { width, height }];
}
@ -43,7 +43,7 @@ const useSize = (
setState(size);
};
const onWindow = (windowToListenOn: Window) => {
windowToListenOn.addEventListener('resize', setSize);
on(windowToListenOn, 'resize', setSize);
DRAF(setSize);
};
@ -61,17 +61,17 @@ const useSize = (
onWindow(window);
} else {
const onLoad = () => {
iframe.removeEventListener('load', onLoad);
on(iframe, 'load', onLoad);
window = iframe.contentWindow!;
onWindow(window);
};
iframe.addEventListener('load', onLoad);
off(iframe, 'load', onLoad);
}
return () => {
if (window && window.removeEventListener) {
window.removeEventListener('resize', setSize);
off(window, 'resize', setSize);
}
};
}, []);

View File

@ -1,6 +1,5 @@
import { useEffect, useRef, RefObject, CSSProperties } from 'react';
import { isClient, off, on } from './util';
import { CSSProperties, RefObject, useEffect, useRef } from 'react';
import { isBrowser, noop, off, on } from './misc/util';
import useMountedState from './useMountedState';
import useSetState from './useSetState';
@ -18,8 +17,6 @@ export interface Options {
vertical?: boolean;
}
const noop = () => {};
const useSlider = (ref: RefObject<HTMLElement>, options: Partial<Options> = {}): State => {
const isMounted = useMountedState();
const isSliding = useRef(false);
@ -33,7 +30,7 @@ const useSlider = (ref: RefObject<HTMLElement>, options: Partial<Options> = {}):
valueRef.current = state.value;
useEffect(() => {
if (isClient) {
if (isBrowser) {
const styles = options.styles === undefined ? true : options.styles;
const reverse = options.reverse === undefined ? false : options.reverse;

View File

@ -1,6 +1,7 @@
import { useRef } from 'react';
import useMount from './useMount';
import useSetState from './useSetState';
import { isBrowser } from './misc/util';
export interface SpeechState {
isPlaying: boolean;
@ -19,8 +20,7 @@ export interface SpeechOptions {
volume?: number;
}
const voices =
typeof window === 'object' && typeof window.speechSynthesis === 'object' ? window.speechSynthesis.getVoices() : [];
const voices = isBrowser && typeof window.speechSynthesis === 'object' ? window.speechSynthesis.getVoices() : [];
const useSpeech = (text: string, opts: SpeechOptions = {}): SpeechState => {
const [state, setState] = useSetState<SpeechState>({

View File

@ -1,4 +1,5 @@
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
import { off, on } from './misc/util';
const isFocusedElementEditable = () => {
const { activeElement, body } = document;
@ -45,9 +46,9 @@ const useStartTyping = (onStartTyping: (event: KeyboardEvent) => void) => {
!isFocusedElementEditable() && isTypedCharGood(event) && onStartTyping(event);
};
document.addEventListener('keydown', keydown);
on(document, 'keydown', keydown);
return () => {
document.removeEventListener('keydown', keydown);
off(document, 'keydown', keydown);
};
}, []);
};

View File

@ -1,6 +1,6 @@
import { Dispatch, useCallback, useMemo, useRef, useState } from 'react';
import { useFirstMountState } from './useFirstMountState';
import { InitialHookState, ResolvableHookState, resolveHookState } from './util/resolveHookState';
import { IHookStateInitAction, IHookStateSetAction, resolveHookState } from './misc/hookState';
interface HistoryState<S> {
history: S[];
@ -11,17 +11,17 @@ interface HistoryState<S> {
go: (position: number) => void;
}
export type UseStateHistoryReturn<S> = [S, Dispatch<ResolvableHookState<S>>, HistoryState<S>];
export type UseStateHistoryReturn<S> = [S, Dispatch<IHookStateSetAction<S>>, HistoryState<S>];
export function useStateWithHistory<S, I extends S>(
initialState: InitialHookState<S>,
initialState: IHookStateInitAction<S>,
capacity?: number,
initialHistory?: I[]
): UseStateHistoryReturn<S>;
export function useStateWithHistory<S = undefined>(): UseStateHistoryReturn<S | undefined>;
export function useStateWithHistory<S, I extends S>(
initialState?: InitialHookState<S>,
initialState?: IHookStateInitAction<S>,
capacity: number = 10,
initialHistory?: I[]
): UseStateHistoryReturn<S> {
@ -55,7 +55,7 @@ export function useStateWithHistory<S, I extends S>(
}
const setState = useCallback(
(newState: ResolvableHookState<S>): void => {
(newState: IHookStateSetAction<S>): void => {
innerSetState((currentState) => {
newState = resolveHookState(newState);
@ -78,7 +78,7 @@ export function useStateWithHistory<S, I extends S>(
});
},
[state, capacity]
) as Dispatch<ResolvableHookState<S>>;
) as Dispatch<IHookStateSetAction<S>>;
const historyState = useMemo(
() => ({

View File

@ -1,10 +1,13 @@
import { useRef, useEffect } from 'react';
import { useEffect, useRef } from 'react';
export interface UseTitleOptions {
restoreOnUnmount?: boolean;
}
const DEFAULT_USE_TITLE_OPTIONS: UseTitleOptions = {
restoreOnUnmount: false,
};
function useTitle(title: string, options: UseTitleOptions = DEFAULT_USE_TITLE_OPTIONS) {
const prevTitleRef = useRef(document.title);
document.title = title;

View File

@ -1,4 +1,4 @@
import { useReducer, Reducer } from 'react';
import { Reducer, useReducer } from 'react';
const toggleReducer = (state: boolean, nextValue?: any) => (typeof nextValue === 'boolean' ? nextValue : !state);

View File

@ -1,4 +1,4 @@
import { useMemo, useRef, useEffect } from 'react';
import { useEffect, useMemo, useRef } from 'react';
export type Race = <P extends Promise<any>, E = any>(promise: P, onError?: (error: E) => void) => P;

View File

@ -1,5 +1,5 @@
import useList, { ListActions } from './useList';
import { InitialHookState } from './util/resolveHookState';
import { IHookStateInitAction } from './misc/hookState';
export interface UpsertListActions<T> extends Omit<ListActions<T>, 'upsert'> {
upsert: (newItem: T) => void;
@ -10,7 +10,7 @@ export interface UpsertListActions<T> extends Omit<ListActions<T>, 'upsert'> {
*/
export default function useUpsert<T>(
predicate: (a: T, b: T) => boolean,
initialList: InitialHookState<T[]> = []
initialList: IHookStateInitAction<T[]> = []
): [T[], UpsertListActions<T>] {
const [list, listActions] = useList(initialList);

View File

@ -1,11 +1,10 @@
import { useEffect } from 'react';
import { noop } from './misc/util';
export type VibrationPattern = number | number[];
const isVibrationApiSupported = typeof navigator === 'object' && 'vibrate' in navigator;
const useVibrateMock = () => {};
function useVibrate(enabled: boolean = true, pattern: VibrationPattern = [1000, 1000], loop: boolean = true): void {
useEffect(() => {
let interval;
@ -34,4 +33,4 @@ function useVibrate(enabled: boolean = true, pattern: VibrationPattern = [1000,
}, [enabled]);
}
export default isVibrationApiSupported ? useVibrate : useVibrateMock;
export default isVibrationApiSupported ? useVibrate : noop;

View File

@ -1,4 +1,4 @@
import createHTMLMediaHook from './util/createHTMLMediaHook';
import createHTMLMediaHook from './factory/createHTMLMediaHook';
const useVideo = createHTMLMediaHook('video');

View File

@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { isClient } from './util';
import { isBrowser, off, on } from './misc/util';
import useRafState from './useRafState';
@ -10,8 +10,8 @@ export interface State {
const useWindowScroll = (): State => {
const [state, setState] = useRafState<State>({
x: isClient ? window.pageXOffset : 0,
y: isClient ? window.pageYOffset : 0,
x: isBrowser ? window.pageXOffset : 0,
y: isBrowser ? window.pageYOffset : 0,
});
useEffect(() => {
@ -22,13 +22,13 @@ const useWindowScroll = (): State => {
});
};
window.addEventListener('scroll', handler, {
on(window, 'scroll', handler, {
capture: false,
passive: true,
});
return () => {
window.removeEventListener('scroll', handler);
off(window, 'scroll', handler);
};
}, []);

View File

@ -1,16 +1,16 @@
import { useEffect } from 'react';
import useRafState from './useRafState';
import { isClient } from './util';
import { isBrowser, off, on } from './misc/util';
const useWindowSize = (initialWidth = Infinity, initialHeight = Infinity) => {
const [state, setState] = useRafState<{ width: number; height: number }>({
width: isClient ? window.innerWidth : initialWidth,
height: isClient ? window.innerHeight : initialHeight,
width: isBrowser ? window.innerWidth : initialWidth,
height: isBrowser ? window.innerHeight : initialHeight,
});
useEffect((): (() => void) | void => {
if (isClient) {
if (isBrowser) {
const handler = () => {
setState({
width: window.innerWidth,
@ -18,10 +18,10 @@ const useWindowSize = (initialWidth = Infinity, initialHeight = Infinity) => {
});
};
window.addEventListener('resize', handler);
on(window, 'resize', handler);
return () => {
window.removeEventListener('resize', handler);
off(window, 'resize', handler);
};
}
}, []);

View File

@ -1,13 +0,0 @@
import isDeepEqualReact from 'fast-deep-equal/react';
export const isClient = typeof window === 'object';
export const on = (obj: any, ...args: any[]) => obj.addEventListener(...args);
export const off = (obj: any, ...args: any[]) => obj.removeEventListener(...args);
export type FnReturningPromise = (...args: any[]) => Promise<any>;
export type PromiseType<P extends Promise<any>> = P extends Promise<infer T> ? T : never;
export const isDeepEqual: (a: any, b: any) => boolean = isDeepEqualReact;

View File

@ -1,237 +0,0 @@
import * as React from 'react';
import { useEffect, useRef } from 'react';
import useSetState from '../useSetState';
import parseTimeRanges from './parseTimeRanges';
export interface HTMLMediaProps extends React.AudioHTMLAttributes<any>, React.VideoHTMLAttributes<any> {
src: string;
}
export interface HTMLMediaState {
buffered: any[];
duration: number;
paused: boolean;
muted: boolean;
time: number;
volume: number;
}
export interface HTMLMediaControls {
play: () => Promise<void> | void;
pause: () => void;
mute: () => void;
unmute: () => void;
volume: (volume: number) => void;
seek: (time: number) => void;
}
type createHTMLMediaHookReturn = [
React.ReactElement<HTMLMediaProps>,
HTMLMediaState,
HTMLMediaControls,
{ current: HTMLAudioElement | null }
];
const createHTMLMediaHook = (tag: 'audio' | 'video') => (
elOrProps: HTMLMediaProps | React.ReactElement<HTMLMediaProps>
): createHTMLMediaHookReturn => {
let element: React.ReactElement<any> | undefined;
let props: HTMLMediaProps;
if (React.isValidElement(elOrProps)) {
element = elOrProps;
props = element.props;
} else {
props = elOrProps as HTMLMediaProps;
}
const [state, setState] = useSetState<HTMLMediaState>({
buffered: [],
time: 0,
duration: 0,
paused: true,
muted: false,
volume: 1,
});
const ref = useRef<HTMLAudioElement | null>(null);
const wrapEvent = (userEvent, proxyEvent?) => {
return (event) => {
try {
proxyEvent && proxyEvent(event);
} finally {
userEvent && userEvent(event);
}
};
};
const onPlay = () => setState({ paused: false });
const onPause = () => setState({ paused: true });
const onVolumeChange = () => {
const el = ref.current;
if (!el) {
return;
}
setState({
muted: el.muted,
volume: el.volume,
});
};
const onDurationChange = () => {
const el = ref.current;
if (!el) {
return;
}
const { duration, buffered } = el;
setState({
duration,
buffered: parseTimeRanges(buffered),
});
};
const onTimeUpdate = () => {
const el = ref.current;
if (!el) {
return;
}
setState({ time: el.currentTime });
};
const onProgress = () => {
const el = ref.current;
if (!el) {
return;
}
setState({ buffered: parseTimeRanges(el.buffered) });
};
if (element) {
element = React.cloneElement(element, {
controls: false,
...props,
ref,
onPlay: wrapEvent(props.onPlay, onPlay),
onPause: wrapEvent(props.onPause, onPause),
onVolumeChange: wrapEvent(props.onVolumeChange, onVolumeChange),
onDurationChange: wrapEvent(props.onDurationChange, onDurationChange),
onTimeUpdate: wrapEvent(props.onTimeUpdate, onTimeUpdate),
onProgress: wrapEvent(props.onProgress, onProgress),
});
} else {
element = React.createElement(tag, {
controls: false,
...props,
ref,
onPlay: wrapEvent(props.onPlay, onPlay),
onPause: wrapEvent(props.onPause, onPause),
onVolumeChange: wrapEvent(props.onVolumeChange, onVolumeChange),
onDurationChange: wrapEvent(props.onDurationChange, onDurationChange),
onTimeUpdate: wrapEvent(props.onTimeUpdate, onTimeUpdate),
onProgress: wrapEvent(props.onProgress, onProgress),
} as any); // TODO: fix this typing.
}
// Some browsers return `Promise` on `.play()` and may throw errors
// if one tries to execute another `.play()` or `.pause()` while that
// promise is resolving. So we prevent that with this lock.
// See: https://bugs.chromium.org/p/chromium/issues/detail?id=593273
let lockPlay: boolean = false;
const controls = {
play: () => {
const el = ref.current;
if (!el) {
return undefined;
}
if (!lockPlay) {
const promise = el.play();
const isPromise = typeof promise === 'object';
if (isPromise) {
lockPlay = true;
const resetLock = () => {
lockPlay = false;
};
promise.then(resetLock, resetLock);
}
return promise;
}
return undefined;
},
pause: () => {
const el = ref.current;
if (el && !lockPlay) {
return el.pause();
}
},
seek: (time: number) => {
const el = ref.current;
if (!el || state.duration === undefined) {
return;
}
time = Math.min(state.duration, Math.max(0, time));
el.currentTime = time;
},
volume: (volume: number) => {
const el = ref.current;
if (!el) {
return;
}
volume = Math.min(1, Math.max(0, volume));
el.volume = volume;
setState({ volume });
},
mute: () => {
const el = ref.current;
if (!el) {
return;
}
el.muted = true;
},
unmute: () => {
const el = ref.current;
if (!el) {
return;
}
el.muted = false;
},
};
useEffect(() => {
const el = ref.current!;
if (!el) {
if (process.env.NODE_ENV !== 'production') {
if (tag === 'audio') {
console.error(
'useAudio() ref to <audio> element is empty at mount. ' +
'It seem you have not rendered the audio element, which it ' +
'returns as the first argument const [audio] = useAudio(...).'
);
} else if (tag === 'video') {
console.error(
'useVideo() ref to <video> element is empty at mount. ' +
'It seem you have not rendered the video element, which it ' +
'returns as the first argument const [video] = useVideo(...).'
);
}
}
return;
}
setState({
volume: el.volume,
muted: el.muted,
paused: el.paused,
});
// Start media, if autoPlay requested.
if (props.autoPlay && el.paused) {
controls.play();
}
}, [props.src]);
return [element, state, controls, ref];
};
export default createHTMLMediaHook;

View File

@ -1,17 +0,0 @@
export type StateSetter<S> = (prevState: S) => S;
export type InitialStateSetter<S> = () => S;
export type InitialHookState<S> = S | InitialStateSetter<S>;
export type HookState<S> = S | StateSetter<S>;
export type ResolvableHookState<S> = S | StateSetter<S> | InitialStateSetter<S>;
export function resolveHookState<S, C extends S>(newState: InitialStateSetter<S>): S;
export function resolveHookState<S, C extends S>(newState: StateSetter<S>, currentState: C): S;
export function resolveHookState<S, C extends S>(newState: ResolvableHookState<S>, currentState?: C): S;
export function resolveHookState<S, C extends S>(newState: ResolvableHookState<S>, currentState?: C): S {
if (typeof newState === 'function') {
return (newState as Function)(currentState);
}
return newState;
}

View File

@ -1,6 +1,6 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import UseKey from '../../src/comps/UseKey';
import UseKey from '../../src/component/UseKey';
storiesOf('Components/<UseKey>', module).add('Demo', () => (
<div>

View File

@ -1,8 +1,8 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useCounter, useCustomCompareEffect } from '../src';
import { isDeepEqual } from '../src/util';
import ShowDocs from './util/ShowDocs';
import isDeepEqual from '../src/misc/isDeepEqual';
const Demo = () => {
const [countNormal, { inc: incNormal }] = useCounter(0);

View File

@ -1,5 +1,5 @@
import { act, renderHook } from '@testing-library/react-hooks';
import createBreakpoint from '../src/createBreakpoint';
import createBreakpoint from '../src/factory/createBreakpoint';
const useBreakpointA = createBreakpoint();
const useBreakpointB = createBreakpoint({ mobileM: 350, laptop: 1024, tablet: 768 });

View File

@ -1,5 +1,5 @@
import { act, renderHook } from '@testing-library/react-hooks';
import createGlobalState from '../src/createGlobalState';
import createGlobalState from '../src/factory/createGlobalState';
describe('useGlobalState', () => {
it('should be defined', () => {

View File

@ -1,5 +1,5 @@
import { renderHook } from '@testing-library/react-hooks';
import createMemo from '../src/createMemo';
import createMemo from '../src/factory/createMemo';
const getDouble = jest.fn((n: number): number => n * 2);

View File

@ -1,7 +1,7 @@
import { act, renderHook } from '@testing-library/react-hooks';
import logger from 'redux-logger';
import thunk from 'redux-thunk';
import createReducer from '../src/createReducer';
import createReducer from '../src/factory/createReducer';
it('should init reducer hook function', () => {
const useSomeReducer = createReducer();

View File

@ -1,7 +1,7 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { act, renderHook } from '@testing-library/react-hooks';
import createReducerContext from '../src/createReducerContext';
import createReducerContext from '../src/factory/createReducerContext';
type Action = 'increment' | 'decrement';

View File

@ -1,7 +1,7 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { act, renderHook } from '@testing-library/react-hooks';
import createStateContext from '../src/createStateContext';
import createStateContext from '../src/factory/createStateContext';
it('should create a hook and a provider', () => {
const [useSharedNumber, SharedNumberProvider] = createStateContext(0);

View File

@ -1,4 +1,4 @@
import { resolveHookState } from '../src/util/resolveHookState';
import { resolveHookState } from '../../src/misc/hookState';
describe('resolveHookState', () => {
it('should defined', () => {
@ -17,10 +17,19 @@ describe('resolveHookState', () => {
expect(spy).toHaveBeenCalled();
});
it('should pass 2nd parameter to function', () => {
const spy = jest.fn();
it('should pass 2nd parameter to function if it awaited', () => {
const spy = jest.fn((n: number) => n);
resolveHookState(spy, 123);
expect(spy).toHaveBeenCalled();
expect(spy.mock.calls[0][0]).toBe(123);
});
it('should not pass 2nd parameter to function if it not awaited', () => {
const spy = jest.fn(() => {
});
/* @ts-expect-error */
resolveHookState(spy, 123);
expect(spy).toHaveBeenCalled();
expect(spy.mock.calls[0].length).toBe(0);
});
});

View File

@ -1,9 +1,8 @@
import writeText from 'copy-to-clipboard';
import { renderHook, act } from '@testing-library/react-hooks';
import { act, renderHook } from '@testing-library/react-hooks';
import { useCopyToClipboard } from '../src';
const valueToRaiseMockException = 'fake input causing exception in copy to clipboard';
jest.mock('copy-to-clipboard', () =>
jest.fn().mockImplementation(input => {
if (input === valueToRaiseMockException) {
@ -12,17 +11,21 @@ jest.mock('copy-to-clipboard', () =>
return true;
})
);
jest.spyOn(global.console, 'error').mockImplementation(() => {});
describe('useCopyToClipboard', () => {
let hook;
let consoleErrorSpy = jest.spyOn(global.console, 'error').mockImplementation(() => {
});
beforeEach(() => {
hook = renderHook(() => useCopyToClipboard());
});
afterAll(() => {
jest.restoreAllMocks();
consoleErrorSpy.mockRestore();
jest.unmock('copy-to-clipboard');
});
it('should be defined ', () => {
@ -95,7 +98,7 @@ describe('useCopyToClipboard', () => {
[state, copyToClipboard] = hook.result.current;
expect(writeText).not.toBeCalled();
expect(console.error).toBeCalled();
expect(consoleErrorSpy).toBeCalled();
expect(state.value).toBe(testValue);
expect(state.noUserInteraction).toBe(true);
expect(state.error).toBeDefined();

View File

@ -1,7 +1,7 @@
import { renderHook } from '@testing-library/react-hooks';
import { useCustomCompareEffect } from '../src';
import { useEffect } from 'react';
import { isDeepEqual } from '../src/util';
import isDeepEqual from '../src/misc/isDeepEqual';
let options = { max: 10 };
const mockEffectNormal = jest.fn();

View File

@ -30,7 +30,7 @@ it('synchronously sets up ResizeObserver listener', () => {
};
const { result } = renderHook(() => useMeasure());
act(() => {
const div = document.createElement('div');
(result.current[0] as UseMeasureRef)(div);
@ -50,12 +50,12 @@ it('tracks rectangle of a DOM element', () => {
};
const { result } = renderHook(() => useMeasure());
act(() => {
const div = document.createElement('div');
(result.current[0] as UseMeasureRef)(div);
});
act(() => {
listener!([{
contentRect: {
@ -94,12 +94,12 @@ it('tracks multiple updates', () => {
};
const { result } = renderHook(() => useMeasure());
act(() => {
const div = document.createElement('div');
(result.current[0] as UseMeasureRef)(div);
});
act(() => {
listener!([{
contentRect: {
@ -125,7 +125,7 @@ it('tracks multiple updates', () => {
left: 1,
right: 1,
});
act(() => {
listener!([{
contentRect: {
@ -163,7 +163,7 @@ it('calls .disconnect() on ResizeObserver when component unmounts', () => {
};
const { result, unmount } = renderHook(() => useMeasure());
act(() => {
const div = document.createElement('div');
(result.current[0] as UseMeasureRef)(div);

View File

@ -1,7 +1,7 @@
import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks';
import { useRef } from 'react';
import { UseStateHistoryReturn, useStateWithHistory } from '../src/useStateWithHistory';
import { InitialHookState } from '../src/util/resolveHookState';
import { IHookStateSetAction } from '../src/misc/hookState';
describe('useStateWithHistory', () => {
it('should be defined', () => {
@ -9,7 +9,7 @@ describe('useStateWithHistory', () => {
});
function getHook<S, I extends S>(
initialState?: InitialHookState<S>,
initialState?: IHookStateSetAction<S>,
initialCapacity?: number,
initialHistory?: I[]
): RenderHookResult<{ state?: S; history?: I[]; capacity?: number }, [UseStateHistoryReturn<S | undefined>, number]> {

View File

@ -1,7 +1,7 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { replaceRaf } from 'raf-stub';
import useWindowSize from '../src/useWindowSize';
import { isClient } from '../src/util';
import { isBrowser } from '../src/misc/util';
declare var requestAnimationFrame: {
reset: () => void;
@ -46,8 +46,8 @@ describe('useWindowSize', () => {
it('should use passed parameters as initial values in case of non-browser use', () => {
const hook = getHook(1, 1);
expect(hook.result.current.height).toBe(isClient ? window.innerHeight : 1);
expect(hook.result.current.width).toBe(isClient ? window.innerWidth : 1);
expect(hook.result.current.height).toBe(isBrowser ? window.innerHeight : 1);
expect(hook.result.current.width).toBe(isBrowser ? window.innerWidth : 1);
});
it('should re-render after height change on closest RAF', () => {

6397
yarn.lock

File diff suppressed because it is too large Load Diff