From a27f09fd367f8b172866b5fcbaf66f9a5a3481bb Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 29 Jan 2021 14:18:23 +0000 Subject: [PATCH] chore: refactoring and rearrangement. More DRY code. Also move non-hooks to separate directories. BREAKING CHANGE: all `create*` factories been moved to `factory` subdirectory and in case direct import should be imported like `react-use/esm/factory/createBreakpoint` BREAKING CHANGE: `comps` directory renamed to `component` --- docs/useKey.md | 2 +- package.json | 32 +- src/{comps => component}/UseKey.tsx | 2 +- src/{ => factory}/createBreakpoint.ts | 7 +- src/{ => factory}/createGlobalState.ts | 4 +- src/factory/createHTMLMediaHook.ts | 235 + src/{ => factory}/createMemo.ts | 0 src/{ => factory}/createReducer.ts | 2 +- src/{ => factory}/createReducerContext.ts | 2 +- src/{util => factory}/createRenderProp.ts | 10 +- src/{ => factory}/createRouter.ts | 0 src/{ => factory}/createStateContext.ts | 2 +- src/index.ts | 12 +- src/misc/hookState.ts | 18 + src/misc/isDeepEqual.ts | 3 + src/{util => misc}/parseTimeRanges.ts | 6 +- src/misc/types.ts | 3 + src/misc/util.ts | 7 + src/useAsync.ts | 4 +- src/useAsyncFn.ts | 17 +- src/useAudio.ts | 2 +- src/useBattery.ts | 5 +- src/useBeforeUnload.ts | 5 +- src/useClickAway.ts | 2 +- src/useCookie.ts | 2 +- src/useCounter.ts | 17 +- src/useDeepCompareEffect.ts | 2 +- src/useDrop.ts | 29 +- src/useDropArea.ts | 2 +- src/useEnsuredForwardedRef.ts | 8 +- src/useError.ts | 2 +- src/useEvent.ts | 10 +- src/useFullscreen.ts | 19 +- src/useGetSet.ts | 10 +- src/useHarmonicIntervalFn.ts | 2 +- src/useHash.ts | 7 +- src/useHover.ts | 3 +- src/useHoverDirty.ts | 9 +- src/useIdle.ts | 2 +- src/useKey.ts | 2 +- src/useList.ts | 8 +- src/useLocalStorage.ts | 8 +- src/useLocation.ts | 6 +- src/useLockBodyScroll.ts | 10 +- src/useLongPress.ts | 5 +- src/useMap.ts | 2 +- src/useMeasure.ts | 14 +- src/useMeasureDirty.ts | 2 +- src/useMedia.ts | 4 +- src/useMediaDevices.ts | 4 +- src/useMethods.ts | 2 +- src/useMotion.ts | 2 +- src/useMouse.ts | 5 +- src/useMouseWheel.ts | 5 +- src/useNetwork.ts | 2 +- src/useOrientation.ts | 2 +- src/usePageLeave.ts | 5 +- src/usePermission.ts | 4 +- src/useRafState.ts | 2 +- src/useScratch.ts | 37 +- src/useScroll.ts | 5 +- src/useScrolling.ts | 5 +- src/useSearchParam.ts | 17 +- src/useSessionStorage.ts | 4 +- src/useSet.ts | 2 +- src/useSize.tsx | 12 +- src/useSlider.ts | 9 +- src/useSpeech.ts | 4 +- src/useStartTyping.ts | 5 +- src/useStateWithHistory.ts | 12 +- src/useTitle.ts | 5 +- src/useToggle.ts | 2 +- src/useUnmountPromise.ts | 2 +- src/useUpsert.ts | 4 +- src/useVibrate.ts | 5 +- src/useVideo.ts | 2 +- src/useWindowScroll.ts | 10 +- src/useWindowSize.ts | 12 +- src/util.ts | 13 - src/util/createHTMLMediaHook.ts | 237 - src/util/resolveHookState.ts | 17 - stories/comps/UseKey.story.tsx | 2 +- stories/useCustomCompareEffect.story.tsx | 2 +- tests/createBreakpoint.test.ts | 2 +- tests/createGlobalState.test.ts | 2 +- tests/createMemo.test.ts | 2 +- tests/createReducer.test.ts | 2 +- tests/createReducerContext.test.tsx | 2 +- tests/createStateContext.test.tsx | 2 +- .../hookState.test.ts} | 15 +- tests/useCopyToClipboard.test.ts | 13 +- tests/useCustomCompareEffect.test.ts | 2 +- tests/useMeasure.test.ts | 14 +- tests/useStateWithHistory.test.ts | 4 +- tests/useWindowSize.test.tsx | 6 +- yarn.lock | 6397 ++++++----------- 96 files changed, 2585 insertions(+), 4903 deletions(-) rename src/{comps => component}/UseKey.tsx (72%) rename src/{ => factory}/createBreakpoint.ts (79%) rename src/{ => factory}/createGlobalState.ts (87%) create mode 100644 src/factory/createHTMLMediaHook.ts rename src/{ => factory}/createMemo.ts (100%) rename src/{ => factory}/createReducer.ts (97%) rename src/{ => factory}/createReducerContext.ts (93%) rename src/{util => factory}/createRenderProp.ts (54%) rename src/{ => factory}/createRouter.ts (100%) rename src/{ => factory}/createStateContext.ts (92%) create mode 100644 src/misc/hookState.ts create mode 100644 src/misc/isDeepEqual.ts rename src/{util => misc}/parseTimeRanges.ts (73%) create mode 100644 src/misc/types.ts create mode 100644 src/misc/util.ts delete mode 100644 src/util.ts delete mode 100644 src/util/createHTMLMediaHook.ts delete mode 100644 src/util/resolveHookState.ts rename tests/{resolveHookState.test.ts => misc/hookState.test.ts} (58%) diff --git a/docs/useKey.md b/docs/useKey.md index bd4c25a8a5..ef626f356f 100644 --- a/docs/useKey.md +++ b/docs/useKey.md @@ -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'; alert('"a" key pressed!')} /> ``` diff --git a/package.json b/package.json index 038ef8c121..dba66fd803 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/comps/UseKey.tsx b/src/component/UseKey.tsx similarity index 72% rename from src/comps/UseKey.tsx rename to src/component/UseKey.tsx index 3ac19e0137..0cf069c4a6 100644 --- a/src/comps/UseKey.tsx +++ b/src/component/UseKey.tsx @@ -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]); diff --git a/src/createBreakpoint.ts b/src/factory/createBreakpoint.ts similarity index 79% rename from src/createBreakpoint.ts rename to src/factory/createBreakpoint.ts index eee72085ee..92f5c301cb 100644 --- a/src/createBreakpoint.ts +++ b/src/factory/createBreakpoint.ts @@ -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)), [ diff --git a/src/createGlobalState.ts b/src/factory/createGlobalState.ts similarity index 87% rename from src/createGlobalState.ts rename to src/factory/createGlobalState.ts index cb76af8a3d..dca1664282 100644 --- a/src/createGlobalState.ts +++ b/src/factory/createGlobalState.ts @@ -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(initialState?: S) { const store: { state: S | undefined; setState: (state: S) => void; setters: any[] } = { diff --git a/src/factory/createHTMLMediaHook.ts b/src/factory/createHTMLMediaHook.ts new file mode 100644 index 0000000000..fbea71b3a1 --- /dev/null +++ b/src/factory/createHTMLMediaHook.ts @@ -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, React.VideoHTMLAttributes { + src: string; +} + +export interface HTMLMediaState { + buffered: any[]; + duration: number; + paused: boolean; + muted: boolean; + time: number; + volume: number; +} + +export interface HTMLMediaControls { + play: () => Promise | void; + pause: () => void; + mute: () => void; + unmute: () => void; + volume: (volume: number) => void; + seek: (time: number) => void; +} + +type createHTMLMediaHookReturn = [ + React.ReactElement, + HTMLMediaState, + HTMLMediaControls, + { current: HTMLAudioElement | null } +]; + +export default function createHTMLMediaHook(tag: 'audio' | 'video') { + return (elOrProps: HTMLMediaProps | React.ReactElement): createHTMLMediaHookReturn => { + let element: React.ReactElement | undefined; + let props: HTMLMediaProps; + + if (React.isValidElement(elOrProps)) { + element = elOrProps; + props = element.props; + } else { + props = elOrProps as HTMLMediaProps; + } + + const [state, setState] = useSetState({ + buffered: [], + time: 0, + duration: 0, + paused: true, + muted: false, + volume: 1, + }); + const ref = useRef(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