├── src ├── useWait.ts ├── useNumber.ts ├── useBoolean.ts ├── misc │ ├── isDeepEqual.ts │ ├── types.ts │ ├── parseTimeRanges.ts │ ├── util.ts │ └── hookState.ts ├── useRendersCount.ts ├── useAudio.ts ├── useVideo.ts ├── useEffectOnce.ts ├── useMount.ts ├── useLatest.ts ├── factory │ ├── createMemo.ts │ ├── createRenderProp.ts │ ├── createRouter.ts │ ├── createStateContext.ts │ └── createBreakpoint.ts ├── useIsomorphicLayoutEffect.ts ├── usePrevious.ts ├── useUpdate.ts ├── component │ └── UseKey.tsx ├── useFirstMountState.ts ├── useLifecycles.ts ├── useTimeout.ts ├── useToggle.ts ├── useUpdateEffect.ts ├── useUnmount.ts ├── useMountedState.ts ├── useError.ts ├── useFavicon.ts ├── useKeyPress.ts ├── useDebounce.ts ├── useDefault.ts ├── useLogger.ts ├── useMouseWheel.ts ├── useSetState.ts ├── useAsync.ts ├── useInterval.ts ├── useScrollbarWidth.ts ├── useKeyPressEvent.ts ├── useHarmonicIntervalFn.ts ├── usePageLeave.ts ├── usePromise.ts ├── useGetSet.ts ├── usePreviousDistinct.ts ├── useRafState.ts ├── useTween.ts ├── useTitle.ts ├── useUpsert.ts ├── useObservable.ts ├── useCookie.ts ├── useQueue.ts ├── useMouseHovered.ts ├── useKeyboardJs.ts ├── useHash.ts ├── useBeforeUnload.ts ├── useWindowSize.ts ├── useDeepCompareEffect.ts ├── useAsyncRetry.ts ├── useScrolling.ts ├── useClickAway.ts ├── useHover.ts ├── useEnsuredForwardedRef.ts ├── useCss.ts ├── useIntersection.ts ├── useGetSetState.ts ├── useRaf.ts ├── useVibrate.ts ├── useSearchParam.ts ├── useThrottleFn.ts ├── useUnmountPromise.ts ├── useThrottle.ts ├── useShallowCompareEffect.ts ├── useMediaDevices.ts └── useTimeoutFn.ts ├── docs ├── Animations.md ├── State.md ├── UI.md ├── Side-effects.md ├── Lifecycles.md ├── Sensors.md ├── useTitle.md ├── usePageLeave.md ├── useFavicon.md ├── useMouseWheel.md ├── useMotion.md ├── useStartTyping.md ├── useMediaDevices.md ├── useWindowScroll.md ├── usePermission.md ├── useHarmonicIntervalFn.md ├── useWindowSize.md ├── useMount.md ├── useUpdate.md ├── useUnmount.md ├── useLocation.md ├── useObservable.md ├── useLifecycles.md ├── useSpeech.md ├── useGetSetState.md ├── useOrientation.md ├── useEffectOnce.md ├── useScroll.md ├── useGeolocation.md ├── useScrolling.md ├── useToggle.md ├── useQueue.md ├── useLogger.md ├── useDefault.md ├── usePromise.md ├── useRendersCount.md ├── useIdle.md ├── useIsomorphicLayoutEffect.md ├── useThrottle.md ├── useError.md ├── useSlider.md ├── useClickAway.md ├── useDeepCompareEffect.md ├── useRaf.md ├── useFirstMountState.md ├── useShallowCompareEffect.md ├── useScrollbarWidth.md ├── useUnmountPromise.md ├── useAsync.md ├── createMemo.md ├── usePrevious.md ├── useVibrate.md ├── useMountedState.md ├── useSet.md ├── useSize.md ├── useFullscreen.md ├── useMap.md ├── useRafState.md ├── useEvent.md ├── useKeyPress.md ├── useTween.md ├── useAsyncRetry.md ├── useUpdateEffect.md ├── useCustomCompareEffect.md ├── useKey.md ├── useSessionStorage.md ├── useAsyncFn.md ├── useMedia.md ├── useCookie.md ├── usePinchZoom.md ├── useHash.md ├── useSpring.md ├── useHover.md ├── useMouse.md ├── useDrop.md ├── useSetState.md ├── useGetSet.md ├── useInterval.md ├── useKeyboardJs.md ├── useLatest.md ├── useCopyToClipboard.md ├── useIntersection.md ├── useSearchParam.md ├── useMethods.md └── useUpsert.md ├── .storybook ├── addons.js ├── config.js └── webpack.config.js ├── stories ├── util │ ├── ConsoleStory.tsx │ ├── CenterStory.tsx │ └── NewTabStory.tsx ├── useIsomorphicLayoutEffect.story.tsx ├── comps │ └── UseKey.story.tsx ├── useTween.story.tsx ├── useRaf.story.tsx ├── useMotion.story.tsx ├── useMedia.story.tsx ├── useGeolocation.story.tsx ├── useOrientation.story.tsx ├── useMediaDevices.story.tsx ├── usePermission.story.tsx ├── useLifecycles.story.tsx ├── useMouseWheel.story.tsx ├── useTitle.story.tsx ├── useUpdate.story.tsx ├── useHoverDirty.story.tsx ├── useCss.story.tsx ├── useMount.story.tsx ├── useWindowSize.story.tsx ├── useSpeech.story.tsx ├── useFavicon.story.tsx ├── useMeasure.story.tsx ├── usePageLeave.story.tsx ├── useObservable.story.tsx ├── useMountedState.story.tsx ├── useScrollbarWidth.story.tsx ├── useHover.story.tsx ├── useUnmount.story.tsx ├── useSize.story.tsx ├── useVibrate.story.tsx ├── useGetSetState.story.tsx ├── useEffectOnce.story.tsx ├── useToggle.story.tsx ├── useSessionStorage.story.tsx ├── useBoolean.story.tsx ├── useSpring.story.tsx ├── createMemo.story.tsx ├── useRendersCount.story.tsx ├── useLocation.story.tsx ├── useLongPress.story.tsx ├── useQueue.story.tsx ├── useClickAway.story.tsx ├── useFirstMountState.story.tsx ├── useNetwork.story.tsx ├── usePrevious.story.tsx ├── useIdle.story.tsx ├── useDefault.story.tsx ├── useUpdateEffect.story.tsx ├── useWindowScroll.story.tsx ├── useMap.story.tsx ├── useHash.story.tsx ├── usePreviousDistinct.story.tsx ├── useScroll.story.tsx ├── useSet.story.tsx ├── useTimeout.story.tsx ├── useLatest.story.tsx ├── useMediatedState.story.tsx ├── useScrolling.story.tsx ├── useSetState.story.tsx ├── useGetSet.story.tsx ├── createGlobalState.story.tsx ├── useCookie.story.tsx ├── useLogger.story.tsx ├── useEvent.story.tsx ├── useDeepCompareEffect.story.tsx ├── useMethods.story.tsx ├── useInterval.story.tsx ├── useKeyPress.story.tsx ├── createBreakpoint.story.tsx ├── useLocalStorage.story.tsx ├── useRafLoop.story.tsx ├── useMouse.story.tsx ├── useStartTyping.story.tsx ├── useRafState.story.tsx ├── useAsyncFn.story.tsx ├── useStateValidator.story.tsx ├── useKeyboardJs.story.tsx ├── useSearchParam.story.tsx ├── useCustomCompareEffect.story.tsx └── useCopyToClipboard.story.tsx ├── tests ├── useBoolean.test.ts ├── useNumber.test.ts ├── setupTests.ts ├── useError.test.ts ├── useUpdateEffect.test.ts ├── usePrevious.test.ts ├── useRendersCount.test.ts ├── useNetworkState.test.ts ├── useFirstMountState.test.ts ├── useEffectOnce.test.ts ├── useLatest.test.ts ├── useMount.test.ts ├── useMountedState.test.tsx ├── useQueue.test.ts ├── useTitle.test.ts └── misc │ └── hookState.test.ts ├── jest.config.ts ├── jest.config.node.ts ├── renovate.json ├── jest.config.base.ts ├── .travis.yml ├── babel.config.js ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── tsconfig.json └── .editorconfig /src/useWait.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/useNumber.ts: -------------------------------------------------------------------------------- 1 | import useNumber from './useCounter'; 2 | 3 | export default useNumber; 4 | -------------------------------------------------------------------------------- /src/useBoolean.ts: -------------------------------------------------------------------------------- 1 | import useBoolean from './useToggle'; 2 | 3 | export default useBoolean; 4 | -------------------------------------------------------------------------------- /docs/Animations.md: -------------------------------------------------------------------------------- 1 | # Animations 2 | 3 | *"Animation Hooks"* usually interpolate numeric values over time. 4 | -------------------------------------------------------------------------------- /docs/State.md: -------------------------------------------------------------------------------- 1 | # State 2 | 3 | *"State Hooks"* allow you to easily manage state of booleans, arrays, and maps. -------------------------------------------------------------------------------- /docs/UI.md: -------------------------------------------------------------------------------- 1 | # UI 2 | 3 | *"UI Hooks"* allow you to control and subscribe to state changes of UI elements. 4 | -------------------------------------------------------------------------------- /src/misc/isDeepEqual.ts: -------------------------------------------------------------------------------- 1 | import isDeepEqualReact from 'fast-deep-equal/react'; 2 | 3 | export default isDeepEqualReact; 4 | -------------------------------------------------------------------------------- /docs/Side-effects.md: -------------------------------------------------------------------------------- 1 | # Side-effects 2 | 3 | *"Side-effect Hooks"* allow your app trigger various side-effects using browser's API. 4 | -------------------------------------------------------------------------------- /src/useRendersCount.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | export function useRendersCount(): number { 4 | return ++useRef(0).current; 5 | } 6 | -------------------------------------------------------------------------------- /docs/Lifecycles.md: -------------------------------------------------------------------------------- 1 | # Lifecycle 2 | 3 | *"Lifecycle Hooks"* modify and extend built-in React hooks or imitate React Class component lifecycle patterns. 4 | -------------------------------------------------------------------------------- /docs/Sensors.md: -------------------------------------------------------------------------------- 1 | # Sensors 2 | 3 | *"Sensor Hooks"* listen to changes in some interface and force your components 4 | to be re-rendered with the new state, up-to-date state. 5 | -------------------------------------------------------------------------------- /src/useAudio.ts: -------------------------------------------------------------------------------- 1 | import createHTMLMediaHook from './factory/createHTMLMediaHook'; 2 | 3 | const useAudio = createHTMLMediaHook('audio'); 4 | export default useAudio; 5 | -------------------------------------------------------------------------------- /src/misc/types.ts: -------------------------------------------------------------------------------- 1 | export type PromiseType

> = P extends Promise ? T : never; 2 | 3 | export type FunctionReturningPromise = (...args: any[]) => Promise; 4 | -------------------------------------------------------------------------------- /src/useVideo.ts: -------------------------------------------------------------------------------- 1 | import createHTMLMediaHook from './factory/createHTMLMediaHook'; 2 | 3 | const useVideo = createHTMLMediaHook('video'); 4 | 5 | export default useVideo; 6 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-knobs/register'; 2 | import '@storybook/addon-options/register'; 3 | import '@storybook/addon-actions/register'; 4 | import '@storybook/addon-notes/register'; 5 | -------------------------------------------------------------------------------- /src/useEffectOnce.ts: -------------------------------------------------------------------------------- 1 | import { EffectCallback, useEffect } from 'react'; 2 | 3 | const useEffectOnce = (effect: EffectCallback) => { 4 | useEffect(effect, []); 5 | }; 6 | 7 | export default useEffectOnce; 8 | -------------------------------------------------------------------------------- /stories/util/ConsoleStory.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const ConsoleStory = ({ message = 'Open the developer console to see logs' }) =>

{message}

; 4 | 5 | export default ConsoleStory; 6 | -------------------------------------------------------------------------------- /src/useMount.ts: -------------------------------------------------------------------------------- 1 | import useEffectOnce from './useEffectOnce'; 2 | 3 | const useMount = (fn: () => void) => { 4 | useEffectOnce(() => { 5 | fn(); 6 | }); 7 | }; 8 | 9 | export default useMount; 10 | -------------------------------------------------------------------------------- /tests/useBoolean.test.ts: -------------------------------------------------------------------------------- 1 | import useBoolean from '../src/useBoolean'; 2 | import useToggle from '../src/useToggle'; 3 | 4 | it('should be an alias for useToggle ', () => { 5 | expect(useBoolean).toBe(useToggle); 6 | }); 7 | -------------------------------------------------------------------------------- /tests/useNumber.test.ts: -------------------------------------------------------------------------------- 1 | import useCounter from '../src/useCounter'; 2 | import useNumber from '../src/useNumber'; 3 | 4 | it('should be an alias for useCounter', () => { 5 | expect(useNumber).toBe(useCounter); 6 | }); 7 | -------------------------------------------------------------------------------- /src/useLatest.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | const useLatest = (value: T): { readonly current: T } => { 4 | const ref = useRef(value); 5 | ref.current = value; 6 | return ref; 7 | }; 8 | 9 | export default useLatest; 10 | -------------------------------------------------------------------------------- /src/factory/createMemo.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | 3 | const createMemo = 4 | any>(fn: T) => 5 | (...args: Parameters) => 6 | useMemo>(() => fn(...args), args); 7 | 8 | export default createMemo; 9 | -------------------------------------------------------------------------------- /src/useIsomorphicLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect } from 'react'; 2 | import { isBrowser } from './misc/util'; 3 | 4 | const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect; 5 | 6 | export default useIsomorphicLayoutEffect; 7 | -------------------------------------------------------------------------------- /tests/setupTests.ts: -------------------------------------------------------------------------------- 1 | import 'jest-localstorage-mock'; 2 | import { isBrowser } from '../src/misc/util'; 3 | 4 | if (isBrowser) { 5 | (window as any).ResizeObserver = class ResizeObserver { 6 | observe() {} 7 | 8 | disconnect() {} 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | import { baseJestConfig } from './jest.config.base'; 3 | 4 | const config: Config.InitialOptions = { 5 | ...baseJestConfig, 6 | testEnvironment: 'jsdom', // browser-like 7 | } 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /src/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export default function usePrevious(state: T): T | undefined { 4 | const ref = useRef(); 5 | 6 | useEffect(() => { 7 | ref.current = state; 8 | }); 9 | 10 | return ref.current; 11 | } 12 | -------------------------------------------------------------------------------- /docs/useTitle.md: -------------------------------------------------------------------------------- 1 | # `useTitle` 2 | 3 | React side-effect hook that sets title of the page. 4 | 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import {useTitle} from 'react-use'; 10 | 11 | const Demo = () => { 12 | useTitle('Hello world!'); 13 | 14 | return null; 15 | }; 16 | ``` 17 | -------------------------------------------------------------------------------- /jest.config.node.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | import { baseJestConfig } from './jest.config.base'; 3 | 4 | const config: Config.InitialOptions = { 5 | ...baseJestConfig, 6 | testEnvironment: 'node', // browser-like 7 | } 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /src/useUpdate.ts: -------------------------------------------------------------------------------- 1 | import { useReducer } from 'react'; 2 | 3 | const updateReducer = (num: number): number => (num + 1) % 1_000_000; 4 | 5 | export default function useUpdate(): () => void { 6 | const [, update] = useReducer(updateReducer, 0); 7 | 8 | return update; 9 | } 10 | -------------------------------------------------------------------------------- /src/component/UseKey.tsx: -------------------------------------------------------------------------------- 1 | import useKey from '../useKey'; 2 | import createRenderProp from '../factory/createRenderProp'; 3 | 4 | const UseKey = createRenderProp(useKey, ({ filter, fn, deps, ...rest }) => [ 5 | filter, 6 | fn, 7 | rest, 8 | deps, 9 | ]); 10 | 11 | export default UseKey; 12 | -------------------------------------------------------------------------------- /src/useFirstMountState.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | export function useFirstMountState(): boolean { 4 | const isFirst = useRef(true); 5 | 6 | if (isFirst.current) { 7 | isFirst.current = false; 8 | 9 | return true; 10 | } 11 | 12 | return isFirst.current; 13 | } 14 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "automerge": true, 6 | "pinVersions": false, 7 | "ignoreUnstable": true, 8 | "major": { 9 | "automerge": false 10 | }, 11 | "devDependencies": { 12 | "automerge": true, 13 | "pinVersions": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docs/usePageLeave.md: -------------------------------------------------------------------------------- 1 | # `usePageLeave` 2 | 3 | React sensor hook that fires a callback when mouse leaves the page. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {usePageLeave} from 'react-use'; 9 | 10 | const Demo = () => { 11 | usePageLeave(() => console.log('Page left...')); 12 | 13 | return null; 14 | }; 15 | ``` 16 | -------------------------------------------------------------------------------- /stories/useIsomorphicLayoutEffect.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import ShowDocs from './util/ShowDocs'; 4 | 5 | storiesOf('Lifecycle/useIsomorphicLayoutEffect', module).add('Docs', () => ( 6 | 7 | )); 8 | -------------------------------------------------------------------------------- /docs/useFavicon.md: -------------------------------------------------------------------------------- 1 | # `useFavicon` 2 | 3 | React side-effect hook sets the favicon of the page. 4 | 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import {useFavicon} from 'react-use'; 10 | 11 | const Demo = () => { 12 | useFavicon('https://cdn.sstatic.net/Sites/stackoverflow/img/favicon.ico'); 13 | 14 | return null; 15 | }; 16 | ``` 17 | -------------------------------------------------------------------------------- /src/misc/parseTimeRanges.ts: -------------------------------------------------------------------------------- 1 | export default function parseTimeRanges(ranges) { 2 | const result: { start: number; end: number }[] = []; 3 | 4 | for (let i = 0; i < ranges.length; i++) { 5 | result.push({ 6 | start: ranges.start(i), 7 | end: ranges.end(i), 8 | }); 9 | } 10 | 11 | return result; 12 | } 13 | -------------------------------------------------------------------------------- /src/useLifecycles.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | const useLifecycles = (mount, unmount?) => { 4 | useEffect(() => { 5 | if (mount) { 6 | mount(); 7 | } 8 | return () => { 9 | if (unmount) { 10 | unmount(); 11 | } 12 | }; 13 | }, []); 14 | }; 15 | 16 | export default useLifecycles; 17 | -------------------------------------------------------------------------------- /src/useTimeout.ts: -------------------------------------------------------------------------------- 1 | import useTimeoutFn from './useTimeoutFn'; 2 | import useUpdate from './useUpdate'; 3 | 4 | export type UseTimeoutReturn = [() => boolean | null, () => void, () => void]; 5 | 6 | export default function useTimeout(ms: number = 0): UseTimeoutReturn { 7 | const update = useUpdate(); 8 | 9 | return useTimeoutFn(update, ms); 10 | } 11 | -------------------------------------------------------------------------------- /stories/comps/UseKey.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import UseKey from '../../src/component/UseKey'; 4 | 5 | storiesOf('Components/', module).add('Demo', () => ( 6 |
7 | Press "q" key! 8 | alert('Q pressed!')} /> 9 |
10 | )); 11 | -------------------------------------------------------------------------------- /docs/useMouseWheel.md: -------------------------------------------------------------------------------- 1 | # `useMouseWheel` 2 | React Hook to get deltaY of mouse scrolled in window. 3 | 4 | ## Usage 5 | 6 | ```jsx 7 | import { useMouseWheel } from 'react-use'; 8 | 9 | const Demo = () => { 10 | const mouseWheel = useMouseWheel() 11 | return ( 12 | <> 13 |

delta Y Scrolled: {mouseWheel}

14 | 15 | ); 16 | }; 17 | ``` 18 | -------------------------------------------------------------------------------- /jest.config.base.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | 3 | export const baseJestConfig: Config.InitialOptions = { 4 | 'preset': 'ts-jest', 5 | 'clearMocks': true, 6 | 'coverageDirectory': 'coverage', 7 | 'testMatch': [ 8 | '/tests/**/*.test.(ts|tsx)' 9 | ], 10 | 'setupFiles': [ 11 | '/tests/setupTests.ts' 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/useMotion.md: -------------------------------------------------------------------------------- 1 | # `useMotion` 2 | 3 | React sensor hook that uses device's acceleration sensor to track its motions. 4 | 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import {useMotion} from 'react-use'; 10 | 11 | const Demo = () => { 12 | const state = useMotion(); 13 | 14 | return ( 15 |
16 |       {JSON.stringify(state, null, 2)}
17 |     
18 | ); 19 | }; 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/useStartTyping.md: -------------------------------------------------------------------------------- 1 | # `useStartTyping` 2 | 3 | React sensor hook that fires a callback when user starts typing. Can be used 4 | to focus default input field on the page. 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import {useStartTyping} from 'react-use'; 10 | 11 | const Demo = () => { 12 | useStartTyping(() => alert('Started typing...')); 13 | 14 | return null; 15 | }; 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/useMediaDevices.md: -------------------------------------------------------------------------------- 1 | # `useMediaDevices` 2 | 3 | React sensor hook that tracks connected hardware devices. 4 | 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import {useMediaDevices} from 'react-use'; 10 | 11 | const Demo = () => { 12 | const state = useMediaDevices(); 13 | 14 | return ( 15 |
16 |       {JSON.stringify(state, null, 2)}
17 |     
18 | ); 19 | }; 20 | ``` 21 | -------------------------------------------------------------------------------- /stories/util/CenterStory.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const CenterStory = ({ children }) => ( 4 |
12 |
{children}
13 |
14 | ); 15 | -------------------------------------------------------------------------------- /docs/useWindowScroll.md: -------------------------------------------------------------------------------- 1 | # `useWindowScroll` 2 | 3 | React sensor hook that re-renders on window scroll. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useWindowScroll} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const {x, y} = useWindowScroll(); 12 | 13 | return ( 14 |
15 |
x: {x}
16 |
y: {y}
17 |
18 | ); 19 | }; 20 | ``` 21 | -------------------------------------------------------------------------------- /src/factory/createRenderProp.ts: -------------------------------------------------------------------------------- 1 | const defaultMapPropsToArgs = (props) => [props]; 2 | 3 | export default function createRenderProp(hook, mapPropsToArgs = defaultMapPropsToArgs) { 4 | return function RenderProp(props) { 5 | const state = hook(...mapPropsToArgs(props)); 6 | const { children, render = children } = props; 7 | return render ? render(state) || null : null; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/useToggle.ts: -------------------------------------------------------------------------------- 1 | import { Reducer, useReducer } from 'react'; 2 | 3 | const toggleReducer = (state: boolean, nextValue?: any) => 4 | typeof nextValue === 'boolean' ? nextValue : !state; 5 | 6 | const useToggle = (initialValue: boolean): [boolean, (nextValue?: any) => void] => { 7 | return useReducer>(toggleReducer, initialValue); 8 | }; 9 | 10 | export default useToggle; 11 | -------------------------------------------------------------------------------- /src/useUpdateEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useFirstMountState } from './useFirstMountState'; 3 | 4 | const useUpdateEffect: typeof useEffect = (effect, deps) => { 5 | const isFirstMount = useFirstMountState(); 6 | 7 | useEffect(() => { 8 | if (!isFirstMount) { 9 | return effect(); 10 | } 11 | }, deps); 12 | }; 13 | 14 | export default useUpdateEffect; 15 | -------------------------------------------------------------------------------- /docs/usePermission.md: -------------------------------------------------------------------------------- 1 | # `usePermission` 2 | 3 | React side-effect hook to query permission status of browser APIs. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {usePermission} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const state = usePermission({ name: 'microphone' }); 12 | 13 | return ( 14 |
15 |       {JSON.stringify(state, null, 2)}
16 |     
17 | ); 18 | }; 19 | ``` 20 | -------------------------------------------------------------------------------- /src/useUnmount.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import useEffectOnce from './useEffectOnce'; 3 | 4 | const useUnmount = (fn: () => any): void => { 5 | const fnRef = useRef(fn); 6 | 7 | // update the ref each render so if it change the newest callback will be invoked 8 | fnRef.current = fn; 9 | 10 | useEffectOnce(() => () => fnRef.current()); 11 | }; 12 | 13 | export default useUnmount; 14 | -------------------------------------------------------------------------------- /docs/useHarmonicIntervalFn.md: -------------------------------------------------------------------------------- 1 | # `useHarmonicIntervalFn` 2 | 3 | Same as [`useInterval`](./useInterval.md) hook, but triggers all effects with the same delay 4 | at the same time. 5 | 6 | For example, this allows you to create ticking clocks on the page which re-render second counter 7 | all at the same time. 8 | 9 | 10 | ## Reference 11 | 12 | ```js 13 | useHarmonicIntervalFn(fn, delay?: number) 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/useWindowSize.md: -------------------------------------------------------------------------------- 1 | # `useWindowSize` 2 | 3 | React sensor hook that tracks dimensions of the browser window. 4 | 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import {useWindowSize} from 'react-use'; 10 | 11 | const Demo = () => { 12 | const {width, height} = useWindowSize(); 13 | 14 | return ( 15 |
16 |
width: {width}
17 |
height: {height}
18 |
19 | ); 20 | }; 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/useMount.md: -------------------------------------------------------------------------------- 1 | # `useMount` 2 | 3 | React lifecycle hook that calls a function after the component is mounted. Use `useLifecycles` if you need both a mount and unmount function. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useMount} from 'react-use'; 9 | 10 | const Demo = () => { 11 | useMount(() => alert('MOUNTED')); 12 | return null; 13 | }; 14 | ``` 15 | 16 | ## Reference 17 | 18 | ```ts 19 | useMount(fn: () => void); 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/useUpdate.md: -------------------------------------------------------------------------------- 1 | # `useUpdate` 2 | 3 | React utility hook that returns a function that forces component 4 | to re-render when called. 5 | 6 | 7 | ## Usage 8 | 9 | ```jsx 10 | import {useUpdate} from 'react-use'; 11 | 12 | const Demo = () => { 13 | const update = useUpdate(); 14 | return ( 15 | <> 16 |
Time: {Date.now()}
17 | 18 | 19 | ); 20 | }; 21 | ``` 22 | -------------------------------------------------------------------------------- /src/useMountedState.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | 3 | export default function useMountedState(): () => boolean { 4 | const mountedRef = useRef(false); 5 | const get = useCallback(() => mountedRef.current, []); 6 | 7 | useEffect(() => { 8 | mountedRef.current = true; 9 | 10 | return () => { 11 | mountedRef.current = false; 12 | }; 13 | }, []); 14 | 15 | return get; 16 | } 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | os: 3 | - linux 4 | cache: 5 | yarn: true 6 | directories: 7 | - ~/.npm 8 | notifications: 9 | email: false 10 | node_js: 11 | - '10' 12 | script: 13 | - yarn lint 14 | - yarn test 15 | - yarn build 16 | - yarn storybook:build 17 | matrix: 18 | allow_failures: [] 19 | fast_finish: true 20 | branches: 21 | except: 22 | - /^v\d+\.\d+\.\d+$/ 23 | - master 24 | - next 25 | - gh-pages 26 | -------------------------------------------------------------------------------- /stories/useTween.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useTween } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const t = useTween(); 8 | 9 | return
Tween: {t}
; 10 | }; 11 | 12 | storiesOf('Animation/useTween', module) 13 | .add('Docs', () => ) 14 | .add('Demo', () => ); 15 | -------------------------------------------------------------------------------- /docs/useUnmount.md: -------------------------------------------------------------------------------- 1 | # `useUnmount` 2 | 3 | React lifecycle hook that calls a function when the component will unmount. Use `useLifecycles` if you need both a mount and unmount function. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useUnmount} from 'react-use'; 9 | 10 | const Demo = () => { 11 | useUnmount(() => alert('UNMOUNTED')); 12 | return null; 13 | }; 14 | ``` 15 | 16 | ## Reference 17 | 18 | ```ts 19 | useUnmount(fn: () => void | undefined); 20 | ``` 21 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | targets: { 7 | node: "current" 8 | } 9 | } 10 | ], 11 | "@babel/preset-react", 12 | "@babel/preset-typescript" 13 | ], 14 | env: { 15 | test: { 16 | plugins: ['dynamic-import-node'] 17 | }, 18 | production: { 19 | plugins: ['@babel/plugin-syntax-dynamic-import'] 20 | } 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /stories/useRaf.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useRaf } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const frames = useRaf(5000, 1000); 8 | 9 | return
Elapsed: {frames}
; 10 | }; 11 | 12 | storiesOf('Animation/useRaf', module) 13 | .add('Docs', () => ) 14 | .add('Demo', () => ); 15 | -------------------------------------------------------------------------------- /src/useError.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react'; 2 | 3 | const useError = (): ((err: Error) => void) => { 4 | const [error, setError] = useState(null); 5 | 6 | useEffect(() => { 7 | if (error) { 8 | throw error; 9 | } 10 | }, [error]); 11 | 12 | const dispatchError = useCallback((err: Error) => { 13 | setError(err); 14 | }, []); 15 | 16 | return dispatchError; 17 | }; 18 | 19 | export default useError; 20 | -------------------------------------------------------------------------------- /docs/useLocation.md: -------------------------------------------------------------------------------- 1 | # `useLocation` 2 | 3 | React sensor hook that tracks brower's location. 4 | 5 | For Internet Explorer you need to [install a polyfill](https://github.com/streamich/react-use/issues/73). 6 | 7 | 8 | ## Usage 9 | 10 | ```jsx 11 | import {useLocation} from 'react-use'; 12 | 13 | const Demo = () => { 14 | const state = useLocation(); 15 | 16 | return ( 17 |
18 |       {JSON.stringify(state, null, 2)}
19 |     
20 | ); 21 | }; 22 | ``` 23 | -------------------------------------------------------------------------------- /src/useFavicon.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | const useFavicon = (href: string) => { 4 | useEffect(() => { 5 | const link: HTMLLinkElement = 6 | document.querySelector("link[rel*='icon']") || document.createElement('link'); 7 | link.type = 'image/x-icon'; 8 | link.rel = 'shortcut icon'; 9 | link.href = href; 10 | document.getElementsByTagName('head')[0].appendChild(link); 11 | }, [href]); 12 | }; 13 | 14 | export default useFavicon; 15 | -------------------------------------------------------------------------------- /src/useKeyPress.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import useKey, { KeyFilter } from './useKey'; 3 | 4 | const useKeyPress = (keyFilter: KeyFilter) => { 5 | const [state, set] = useState<[boolean, null | KeyboardEvent]>([false, null]); 6 | useKey(keyFilter, (event) => set([true, event]), { event: 'keydown' }, [state]); 7 | useKey(keyFilter, (event) => set([false, event]), { event: 'keyup' }, [state]); 8 | return state; 9 | }; 10 | 11 | export default useKeyPress; 12 | -------------------------------------------------------------------------------- /docs/useObservable.md: -------------------------------------------------------------------------------- 1 | # `useObservable` 2 | 3 | React state hook that tracks the latest value of an `Observable`. 4 | 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import {useObservable} from 'react-use'; 10 | 11 | const counter$ = new BehaviorSubject(0); 12 | const Demo = () => { 13 | const value = useObservable(counter$, 0); 14 | 15 | return ( 16 | 19 | ); 20 | }; 21 | ``` 22 | -------------------------------------------------------------------------------- /src/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { DependencyList, useEffect } from 'react'; 2 | import useTimeoutFn from './useTimeoutFn'; 3 | 4 | export type UseDebounceReturn = [() => boolean | null, () => void]; 5 | 6 | export default function useDebounce( 7 | fn: Function, 8 | ms: number = 0, 9 | deps: DependencyList = [] 10 | ): UseDebounceReturn { 11 | const [isReady, cancel, reset] = useTimeoutFn(fn, ms); 12 | 13 | useEffect(reset, deps); 14 | 15 | return [isReady, cancel]; 16 | } 17 | -------------------------------------------------------------------------------- /src/useDefault.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | const useDefault = ( 4 | defaultValue: TStateType, 5 | initialValue: TStateType | (() => TStateType) 6 | ) => { 7 | const [value, setValue] = useState(initialValue); 8 | 9 | if (value === undefined || value === null) { 10 | return [defaultValue, setValue] as const; 11 | } 12 | 13 | return [value, setValue] as const; 14 | }; 15 | 16 | export default useDefault; 17 | -------------------------------------------------------------------------------- /src/useLogger.ts: -------------------------------------------------------------------------------- 1 | import useEffectOnce from './useEffectOnce'; 2 | import useUpdateEffect from './useUpdateEffect'; 3 | 4 | const useLogger = (componentName: string, ...rest) => { 5 | useEffectOnce(() => { 6 | console.log(`${componentName} mounted`, ...rest); 7 | return () => console.log(`${componentName} unmounted`); 8 | }); 9 | 10 | useUpdateEffect(() => { 11 | console.log(`${componentName} updated`, ...rest); 12 | }); 13 | }; 14 | 15 | export default useLogger; 16 | -------------------------------------------------------------------------------- /stories/useMotion.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useMotion } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const state = useMotion(); 8 | 9 | return
{JSON.stringify(state, null, 2)}
; 10 | }; 11 | 12 | storiesOf('Sensors/useMotion', module) 13 | .add('Docs', () => ) 14 | .add('Demo', () => ); 15 | -------------------------------------------------------------------------------- /docs/useLifecycles.md: -------------------------------------------------------------------------------- 1 | # `useLifecycles` 2 | 3 | React lifecycle hook that call `mount` and `unmount` callbacks, when 4 | component is mounted and un-mounted, respectively. 5 | 6 | 7 | ## Usage 8 | 9 | ```jsx 10 | import {useLifecycles} from 'react-use'; 11 | 12 | const Demo = () => { 13 | useLifecycles(() => console.log('MOUNTED'), () => console.log('UNMOUNTED')); 14 | return null; 15 | }; 16 | ``` 17 | 18 | 19 | ## Reference 20 | 21 | ```js 22 | useLifecycles(mount, unmount); 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/useSpeech.md: -------------------------------------------------------------------------------- 1 | # `useSpeech` 2 | 3 | React UI hook that synthesizes human voice that speaks a given string. 4 | 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import {useSpeech} from 'react-use'; 10 | 11 | const voices = window.speechSynthesis.getVoices(); 12 | 13 | const Demo = () => { 14 | const state = useSpeech('Hello world!', { rate: 0.8, pitch: 0.5, voice: voices[0] }); 15 | 16 | return ( 17 |
18 |       {JSON.stringify(state, null, 2)}
19 |     
20 | ); 21 | }; 22 | ``` 23 | -------------------------------------------------------------------------------- /stories/util/NewTabStory.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const NewTabStory = ({ children }) => { 4 | if (window.location === window.parent.location) { 5 | return children; 6 | } 7 | 8 | return ( 9 |

10 | This story should be{' '} 11 | 12 | opened in a new tab 13 | 14 | . 15 |

16 | ); 17 | }; 18 | 19 | export default NewTabStory; 20 | -------------------------------------------------------------------------------- /docs/useGetSetState.md: -------------------------------------------------------------------------------- 1 | # `useGetSetState` 2 | 3 | A mix of `useGetSet` and `useGetSetState`. 4 | 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import {useGetSetState} from 'react-use'; 10 | 11 | const Demo = () => { 12 | const [get, setState] = useGetSetState({cnt: 0}); 13 | const onClick = () => { 14 | setTimeout(() => { 15 | setState({cnt: get().cnt + 1}) 16 | }, 1000); 17 | }; 18 | 19 | return ( 20 | 21 | ); 22 | }; 23 | ``` 24 | -------------------------------------------------------------------------------- /stories/useMedia.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useMedia } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const isWide = useMedia('(min-width: 480px)'); 8 | 9 | return
Screen is wide: {isWide ? 'Yes' : 'No'}
; 10 | }; 11 | 12 | storiesOf('Sensors/useMedia', module) 13 | .add('Docs', () => ) 14 | .add('Demo', () => ); 15 | -------------------------------------------------------------------------------- /stories/useGeolocation.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useGeolocation } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const state = useGeolocation(); 8 | 9 | return
{JSON.stringify(state, null, 2)}
; 10 | }; 11 | 12 | storiesOf('Sensors/useGeolocation', module) 13 | .add('Docs', () => ) 14 | .add('Demo', () => ); 15 | -------------------------------------------------------------------------------- /stories/useOrientation.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useOrientation } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const state = useOrientation(); 8 | 9 | return
{JSON.stringify(state, null, 2)}
; 10 | }; 11 | 12 | storiesOf('Sensors/useOrientation', module) 13 | .add('Docs', () => ) 14 | .add('Demo', () => ); 15 | -------------------------------------------------------------------------------- /stories/useMediaDevices.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useMediaDevices } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const state = useMediaDevices(); 8 | 9 | return
{JSON.stringify(state, null, 2)}
; 10 | }; 11 | 12 | storiesOf('Sensors/useMediaDevices', module) 13 | .add('Docs', () => ) 14 | .add('Demo', () => ); 15 | -------------------------------------------------------------------------------- /src/useMouseWheel.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { off, on } from './misc/util'; 3 | 4 | export default () => { 5 | const [mouseWheelScrolled, setMouseWheelScrolled] = useState(0); 6 | useEffect(() => { 7 | const updateScroll = (e: MouseWheelEvent) => { 8 | setMouseWheelScrolled(e.deltaY + mouseWheelScrolled); 9 | }; 10 | on(window, 'wheel', updateScroll, false); 11 | return () => off(window, 'wheel', updateScroll); 12 | }); 13 | return mouseWheelScrolled; 14 | }; 15 | -------------------------------------------------------------------------------- /docs/useOrientation.md: -------------------------------------------------------------------------------- 1 | # `useOrientation` 2 | 3 | React sensor hook that tracks screen orientation of user's device. 4 | 5 | Returns state in the following shape 6 | 7 | ```js 8 | { 9 | angle: 0, 10 | type: 'landscape-primary' 11 | } 12 | ``` 13 | 14 | 15 | ## Usage 16 | 17 | ```jsx 18 | import {useOrientation} from 'react-use'; 19 | 20 | const Demo = () => { 21 | const state = useOrientation(); 22 | 23 | return ( 24 |
25 |       {JSON.stringify(state, null, 2)}
26 |     
27 | ); 28 | }; 29 | ``` 30 | -------------------------------------------------------------------------------- /stories/usePermission.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { usePermission } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const state = usePermission({ name: 'microphone' }); 8 | 9 | return
{JSON.stringify(state, null, 2)}
; 10 | }; 11 | 12 | storiesOf('Side effects/usePermission', module) 13 | .add('Docs', () => ) 14 | .add('Demo', () => ); 15 | -------------------------------------------------------------------------------- /stories/useLifecycles.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useLifecycles } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | useLifecycles( 8 | () => console.log('MOUNTED'), 9 | () => console.log('UNMOUNTED') 10 | ); 11 | return null; 12 | }; 13 | 14 | storiesOf('Lifecycle/useLifecycles', module) 15 | .add('Docs', () => ) 16 | .add('Demo', () => ); 17 | -------------------------------------------------------------------------------- /docs/useEffectOnce.md: -------------------------------------------------------------------------------- 1 | # `useEffectOnce` 2 | 3 | React lifecycle hook that runs an effect only once. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useEffectOnce} from 'react-use'; 9 | 10 | const Demo = () => { 11 | useEffectOnce(() => { 12 | console.log('Running effect once on mount') 13 | 14 | return () => { 15 | console.log('Running clean-up of effect on unmount') 16 | } 17 | }); 18 | 19 | return null; 20 | }; 21 | ``` 22 | 23 | ## Reference 24 | 25 | ```js 26 | useEffectOnce(effect: EffectCallback); 27 | ``` 28 | -------------------------------------------------------------------------------- /stories/useMouseWheel.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useMouseWheel } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const mouseWheel = useMouseWheel(); 8 | return ( 9 | <> 10 |

delta Y Scrolled: {mouseWheel}

11 | 12 | ); 13 | }; 14 | 15 | storiesOf('Sensors/useMouseWheel', module) 16 | .add('Docs', () => ) 17 | .add('Demo', () => ); 18 | -------------------------------------------------------------------------------- /src/useSetState.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | 3 | const useSetState = ( 4 | initialState: T = {} as T 5 | ): [T, (patch: Partial | ((prevState: T) => Partial)) => void] => { 6 | const [state, set] = useState(initialState); 7 | const setState = useCallback((patch) => { 8 | set((prevState) => 9 | Object.assign({}, prevState, patch instanceof Function ? patch(prevState) : patch) 10 | ); 11 | }, []); 12 | 13 | return [state, setState]; 14 | }; 15 | 16 | export default useSetState; 17 | -------------------------------------------------------------------------------- /stories/useTitle.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useTitle } from '../src'; 4 | import NewTabStory from './util/NewTabStory'; 5 | import ShowDocs from './util/ShowDocs'; 6 | 7 | const Demo = () => { 8 | useTitle('Hello world!'); 9 | 10 | return Title should be "Hello world!"; 11 | }; 12 | 13 | storiesOf('Side effects/useTitle', module) 14 | .add('Docs', () => ) 15 | .add('Demo', () => ); 16 | -------------------------------------------------------------------------------- /docs/useScroll.md: -------------------------------------------------------------------------------- 1 | # `useScroll` 2 | 3 | React sensor hook that re-renders when the scroll position in a DOM element changes. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useScroll} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const scrollRef = React.useRef(null); 12 | const {x, y} = useScroll(scrollRef); 13 | 14 | return ( 15 |
16 |
x: {x}
17 |
y: {y}
18 |
19 | ); 20 | }; 21 | ``` 22 | 23 | ## Reference 24 | 25 | ```ts 26 | useScroll(ref: RefObject); 27 | ``` 28 | -------------------------------------------------------------------------------- /src/useAsync.ts: -------------------------------------------------------------------------------- 1 | import { DependencyList, useEffect } from 'react'; 2 | import useAsyncFn from './useAsyncFn'; 3 | import { FunctionReturningPromise } from './misc/types'; 4 | 5 | export { AsyncState, AsyncFnReturn } from './useAsyncFn'; 6 | 7 | export default function useAsync( 8 | fn: T, 9 | deps: DependencyList = [] 10 | ) { 11 | const [state, callback] = useAsyncFn(fn, deps, { 12 | loading: true, 13 | }); 14 | 15 | useEffect(() => { 16 | callback(); 17 | }, [callback]); 18 | 19 | return state; 20 | } 21 | -------------------------------------------------------------------------------- /stories/useUpdate.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useUpdate } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const update = useUpdate(); 8 | return ( 9 | <> 10 |
Time: {Date.now()}
11 | 12 | 13 | ); 14 | }; 15 | 16 | storiesOf('Animation/useUpdate', module) 17 | .add('Docs', () => ) 18 | .add('Demo', () => ); 19 | -------------------------------------------------------------------------------- /docs/useGeolocation.md: -------------------------------------------------------------------------------- 1 | # `useGeolocation` 2 | 3 | React sensor hook that tracks user's geographic location. This hook accepts [position options](https://developer.mozilla.org/docs/Web/API/PositionOptions). 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useGeolocation} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const state = useGeolocation(); 12 | 13 | return ( 14 |
15 |       {JSON.stringify(state, null, 2)}
16 |     
17 | ); 18 | }; 19 | ``` 20 | 21 | ## Reference 22 | 23 | ```ts 24 | useGeolocation(options: PositionOptions) 25 | ``` 26 | -------------------------------------------------------------------------------- /src/useInterval.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | const useInterval = (callback: Function, delay?: number | null) => { 4 | const savedCallback = useRef(() => {}); 5 | 6 | useEffect(() => { 7 | savedCallback.current = callback; 8 | }); 9 | 10 | useEffect(() => { 11 | if (delay !== null) { 12 | const interval = setInterval(() => savedCallback.current(), delay || 0); 13 | return () => clearInterval(interval); 14 | } 15 | 16 | return undefined; 17 | }, [delay]); 18 | }; 19 | 20 | export default useInterval; 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['react-app', 'prettier'], 3 | plugins: ['prettier'], 4 | rules: { 5 | 'prettier/prettier': [ 6 | 'error', 7 | { 8 | singleQuote: true, 9 | trailingComma: 'es5', 10 | tabWidth: 2, 11 | printWidth: 100, 12 | semicolons: true, 13 | quoteProps: 'as-needed', 14 | jsxSingleQuote: false, 15 | bracketSpacing: true, 16 | jsxBracketSameLine: true, 17 | arrowParens: 'always', 18 | endOfLine: 'lf', 19 | }, 20 | ], 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /stories/useHoverDirty.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useRef } from 'react'; 4 | import { useHoverDirty } from '../src'; 5 | import ShowDocs from './util/ShowDocs'; 6 | 7 | const Demo = () => { 8 | const ref = useRef(null); 9 | const isHovered = useHoverDirty(ref); 10 | 11 | return
{isHovered ? '😁' : '☹️'}
; 12 | }; 13 | 14 | storiesOf('Sensors/useHoverDirty', module) 15 | .add('Docs', () => ) 16 | .add('Demo', () => ); 17 | -------------------------------------------------------------------------------- /docs/useScrolling.md: -------------------------------------------------------------------------------- 1 | # `useScrolling` 2 | 3 | React sensor hook that keeps track of whether the user is scrolling or not. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import { useScrolling } from "react-use"; 9 | 10 | const Demo = () => { 11 | const scrollRef = React.useRef(null); 12 | const scrolling = useScrolling(scrollRef); 13 | 14 | return ( 15 |
16 | {
{scrolling ? "Scrolling" : "Not scrolling"}
} 17 |
18 | ); 19 | }; 20 | ``` 21 | 22 | ## Reference 23 | 24 | ```ts 25 | useScrolling(ref: RefObject); 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/useToggle.md: -------------------------------------------------------------------------------- 1 | # `useToggle` 2 | 3 | React state hook that tracks value of a boolean. 4 | 5 | `useBoolean` is an alias for `useToggle`. 6 | 7 | ## Usage 8 | 9 | ```jsx 10 | import {useToggle} from 'react-use'; 11 | 12 | const Demo = () => { 13 | const [on, toggle] = useToggle(true); 14 | 15 | return ( 16 |
17 |
{on ? 'ON' : 'OFF'}
18 | 19 | 20 | 21 |
22 | ); 23 | }; 24 | ``` 25 | -------------------------------------------------------------------------------- /stories/useCss.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useCss } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const className = useCss({ 8 | color: 'red', 9 | border: '1px solid red', 10 | '&:hover': { 11 | color: 'blue', 12 | }, 13 | }); 14 | 15 | return
hello
; 16 | }; 17 | 18 | storiesOf('UI/useCss', module) 19 | .add('Docs', () => ) 20 | .add('Demo', () => ); 21 | -------------------------------------------------------------------------------- /stories/useMount.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useMount } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | useMount(() => alert('MOUNTED')); 8 | 9 | return ( 10 |
11 | useMount() hook can be used to perform a side-effect when component is mounted. 12 |
13 | ); 14 | }; 15 | 16 | storiesOf('Lifecycle/useMount', module) 17 | .add('Docs', () => ) 18 | .add('Demo', () => ); 19 | -------------------------------------------------------------------------------- /stories/useWindowSize.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useWindowSize } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const { width, height } = useWindowSize(); 8 | 9 | return ( 10 |
11 |
width: {width}
12 |
height: {height}
13 |
14 | ); 15 | }; 16 | 17 | storiesOf('Sensors/useWindowSize', module) 18 | .add('Docs', () => ) 19 | .add('Demo', () => ); 20 | -------------------------------------------------------------------------------- /stories/useSpeech.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useSpeech } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const voices = window.speechSynthesis.getVoices(); 8 | const state = useSpeech('Hello world!', { rate: 0.8, pitch: 0.5, voice: voices[0] }); 9 | 10 | return
{JSON.stringify(state, null, 2)}
; 11 | }; 12 | 13 | storiesOf('UI/useSpeech', module) 14 | .add('Docs', () => ) 15 | .add('Demo', () => ); 16 | -------------------------------------------------------------------------------- /docs/useQueue.md: -------------------------------------------------------------------------------- 1 | # `useQueue` 2 | 3 | React state hook implements simple FIFO queue. 4 | 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import { useQueue } from 'react-use'; 10 | 11 | const Demo = () => { 12 | const { add, remove, first, last, size } = useQueue(); 13 | 14 | return ( 15 |
16 |
    17 |
  • first: {first}
  • 18 |
  • last: {last}
  • 19 |
  • size: {size}
  • 20 |
21 | 22 | 23 |
24 | ); 25 | }; 26 | ``` 27 | -------------------------------------------------------------------------------- /stories/useFavicon.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useFavicon } from '../src'; 4 | import NewTabStory from './util/NewTabStory'; 5 | import ShowDocs from './util/ShowDocs'; 6 | 7 | const Demo = () => { 8 | useFavicon('https://cdn.sstatic.net/Sites/stackoverflow/img/favicon.ico'); 9 | 10 | return Favicon should be the Stack Overflow logo; 11 | }; 12 | 13 | storiesOf('Side effects/useFavicon', module) 14 | .add('Docs', () => ) 15 | .add('Demo', () => ); 16 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import {configure} from '@storybook/react'; 2 | import {setOptions} from '@storybook/addon-options'; 3 | 4 | setOptions({ 5 | sortStoriesByKind: false, 6 | showStoriesPanel: true, 7 | showAddonPanel: true, 8 | showSearchBox: false, 9 | addonPanelInRight: true, 10 | hierarchySeparator: /\//, 11 | hierarchyRootSeparator: /\|/, 12 | sidebarAnimations: false, 13 | }); 14 | 15 | const req = require.context('../stories/', true, /\.story\.tsx?$/); 16 | 17 | const loadStories = () => { 18 | req.keys().forEach((filename) => req(filename)); 19 | }; 20 | 21 | configure(loadStories, module); 22 | -------------------------------------------------------------------------------- /docs/useLogger.md: -------------------------------------------------------------------------------- 1 | # `useLogger` 2 | 3 | React lifecycle hook that console logs parameters as component transitions through lifecycles. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useLogger} from 'react-use'; 9 | 10 | const Demo = (props) => { 11 | useLogger('Demo', props); 12 | return null; 13 | }; 14 | ``` 15 | 16 | ## Example Output 17 | 18 | ``` 19 | Demo mounted {} 20 | Demo updated {} 21 | Demo unmounted 22 | ``` 23 | 24 | ## Reference 25 | 26 | ```js 27 | useLogger(componentName: string, ...rest); 28 | ``` 29 | 30 | - `componentName` — component name. 31 | - `...rest` — parameters to log. 32 | -------------------------------------------------------------------------------- /stories/useMeasure.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import React from 'react'; 3 | import { useMeasure } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [ref, state] = useMeasure(); 8 | 9 | return ( 10 | <> 11 |
{JSON.stringify(state, null, 2)}
12 |
13 | resize me 14 |
15 | 16 | ); 17 | }; 18 | 19 | storiesOf('Sensors/useMeasure', module) 20 | .add('Docs', () => ) 21 | .add('Demo', () => ); 22 | -------------------------------------------------------------------------------- /stories/usePageLeave.story.tsx: -------------------------------------------------------------------------------- 1 | import { action } from '@storybook/addon-actions'; 2 | import { storiesOf } from '@storybook/react'; 3 | import * as React from 'react'; 4 | import { usePageLeave } from '../src'; 5 | import ShowDocs from './util/ShowDocs'; 6 | 7 | const Demo = () => { 8 | usePageLeave(action('onPageLeave')); 9 | 10 | return ( 11 |
12 | Try leaving the page and see logs in Actions tab. 13 |
14 | ); 15 | }; 16 | 17 | storiesOf('Sensors/usePageLeave', module) 18 | .add('Docs', () => ) 19 | .add('Default', () => ); 20 | -------------------------------------------------------------------------------- /stories/useObservable.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { BehaviorSubject } from 'rxjs'; 4 | import { useObservable } from '../src'; 5 | import ShowDocs from './util/ShowDocs'; 6 | 7 | const counter$ = new BehaviorSubject(0); 8 | const Demo = () => { 9 | const value = useObservable(counter$, 0); 10 | 11 | return ; 12 | }; 13 | 14 | storiesOf('State/useObservable', module) 15 | .add('Docs', () => ) 16 | .add('Demo', () => ); 17 | -------------------------------------------------------------------------------- /stories/useMountedState.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useMountedState } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const isMounted = useMountedState(); 8 | const [, updateState] = React.useState(); 9 | 10 | requestAnimationFrame(updateState); 11 | 12 | return
This component is {isMounted() ? 'MOUNTED' : 'NOT MOUNTED'}
; 13 | }; 14 | 15 | storiesOf('Lifecycle/useMountedState', module) 16 | .add('Docs', () => ) 17 | .add('Demo', () => ); 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Have an idea? Great! Let us know, maybe we`ve been waiting only for you =) 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | -------------------------------------------------------------------------------- /docs/useDefault.md: -------------------------------------------------------------------------------- 1 | # `useDefault` 2 | 3 | React state hook that returns the default value when state is null or undefined. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useDefault} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const initialUser = { name: 'Marshall' } 12 | const defaultUser = { name: 'Mathers' } 13 | const [user, setUser] = useDefault(defaultUser, initialUser); 14 | 15 | return ( 16 |
17 |
User: {user.name}
18 | setUser({ name: e.target.value })} /> 19 | 20 |
21 | ); 22 | }; 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/usePromise.md: -------------------------------------------------------------------------------- 1 | # `usePromise` 2 | 3 | React Lifecycle hook that returns a helper function for wrapping promises. 4 | Promises wrapped with this function will resolve only when component is mounted. 5 | 6 | 7 | ## Usage 8 | 9 | ```jsx 10 | import {usePromise} from 'react-use'; 11 | 12 | const Demo = ({promise}) => { 13 | const mounted = usePromise(); 14 | const [value, setValue] = useState(); 15 | 16 | useEffect(() => { 17 | (async () => { 18 | const value = await mounted(promise); 19 | // This line will not execute if component gets unmounted. 20 | setValue(value); 21 | })(); 22 | }); 23 | }; 24 | ``` 25 | -------------------------------------------------------------------------------- /src/useScrollbarWidth.ts: -------------------------------------------------------------------------------- 1 | import { scrollbarWidth } from '@xobotyi/scrollbar-width'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | export function useScrollbarWidth(): number | undefined { 5 | const [sbw, setSbw] = useState(scrollbarWidth()); 6 | 7 | // this needed to ensure the scrollbar width in case hook called before the DOM is ready 8 | useEffect(() => { 9 | if (typeof sbw !== 'undefined') { 10 | return; 11 | } 12 | 13 | const raf = requestAnimationFrame(() => { 14 | setSbw(scrollbarWidth()); 15 | }); 16 | 17 | return () => cancelAnimationFrame(raf); 18 | }, []); 19 | 20 | return sbw; 21 | } 22 | -------------------------------------------------------------------------------- /stories/useScrollbarWidth.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useScrollbarWidth } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const sbw = useScrollbarWidth(); 8 | 9 | return ( 10 |
11 | {sbw === undefined 12 | ? `DOM is not ready yet, SBW detection delayed` 13 | : `Browser's scrollbar width is ${sbw}px`} 14 |
15 | ); 16 | }; 17 | 18 | storiesOf('Sensors/useScrollbarWidth', module) 19 | .add('Docs', () => ) 20 | .add('Demo', () => ); 21 | -------------------------------------------------------------------------------- /stories/useHover.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useHover } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const element = (hasHovered: boolean) =>
Hover me! {hasHovered && 'Thanks!'}
; 8 | const [hoverable, hovered] = useHover(element); 9 | 10 | return ( 11 |
12 | {hoverable} 13 |
{hovered ? 'HOVERED' : ''}
14 |
15 | ); 16 | }; 17 | 18 | storiesOf('Sensors/useHover', module) 19 | .add('Docs', () => ) 20 | .add('Demo', () => ); 21 | -------------------------------------------------------------------------------- /stories/useUnmount.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useUnmount } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | useUnmount(() => alert('UNMOUNTED')); 8 | 9 | return ( 10 |
11 | useUnmount() hook can be used to perform side-effects when component unmounts. 12 | This component will alert you when it is un-mounted. 13 |
14 | ); 15 | }; 16 | 17 | storiesOf('Lifecycle/useUnmount', module) 18 | .add('Docs', () => ) 19 | .add('Demo', () => ); 20 | -------------------------------------------------------------------------------- /docs/useRendersCount.md: -------------------------------------------------------------------------------- 1 | # `useRendersCount` 2 | 3 | Tracks component's renders count including the first render. 4 | 5 | ## Usage 6 | 7 | ```typescript jsx 8 | import * as React from 'react'; 9 | import { useRendersCount } from "react-use"; 10 | 11 | const Demo = () => { 12 | const update = useUpdate(); 13 | const rendersCount = useRendersCount(); 14 | 15 | return ( 16 |
17 | Renders count: {rendersCount} 18 |
19 | 20 |
21 | ); 22 | }; 23 | ``` 24 | 25 | ## Reference 26 | 27 | ```typescript 28 | const rendersCount: number = useRendersCount(); 29 | ``` 30 | -------------------------------------------------------------------------------- /src/useKeyPressEvent.ts: -------------------------------------------------------------------------------- 1 | import { Handler, KeyFilter } from './useKey'; 2 | import useKeyPressDefault from './useKeyPress'; 3 | import useUpdateEffect from './useUpdateEffect'; 4 | 5 | const useKeyPressEvent = ( 6 | key: string | KeyFilter, 7 | keydown?: Handler | null | undefined, 8 | keyup?: Handler | null | undefined, 9 | useKeyPress = useKeyPressDefault 10 | ) => { 11 | const [pressed, event] = useKeyPress(key); 12 | useUpdateEffect(() => { 13 | if (!pressed && keyup) { 14 | keyup(event!); 15 | } else if (pressed && keydown) { 16 | keydown(event!); 17 | } 18 | }, [pressed]); 19 | }; 20 | 21 | export default useKeyPressEvent; 22 | -------------------------------------------------------------------------------- /docs/useIdle.md: -------------------------------------------------------------------------------- 1 | # `useIdle` 2 | 3 | React sensor hook that tracks if user on the page is idle. 4 | 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import {useIdle} from 'react-use'; 10 | 11 | const Demo = () => { 12 | const isIdle = useIdle(3e3); 13 | 14 | return ( 15 |
16 |
User is idle: {isIdle ? 'Yes 😴' : 'Nope'}
17 |
18 | ); 19 | }; 20 | ``` 21 | 22 | 23 | ## Reference 24 | 25 | ```js 26 | useIdle(ms, initialState); 27 | ``` 28 | 29 | - `ms` — time in milliseconds after which to consider use idle, defaults to `60e3` — one minute. 30 | - `initialState` — whether to consider user initially idle, defaults to false. 31 | -------------------------------------------------------------------------------- /docs/useIsomorphicLayoutEffect.md: -------------------------------------------------------------------------------- 1 | # `useIsomorphicLayoutEffect` 2 | 3 | `useLayoutEffect` that does not show warning when server-side rendering, see [Alex Reardon's article](https://medium.com/@alexandereardon/uselayouteffect-and-ssr-192986cdcf7a) for more info. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useIsomorphicLayoutEffect} from 'react-use'; 9 | 10 | const Demo = ({value}) => { 11 | useIsomorphicLayoutEffect(() => { 12 | window.console.log(value) 13 | }, [value]); 14 | 15 | return null; 16 | }; 17 | ``` 18 | 19 | 20 | ## Reference 21 | 22 | ```ts 23 | useIsomorphicLayoutEffect(effect: EffectCallback, deps?: ReadonlyArray | undefined); 24 | ``` 25 | -------------------------------------------------------------------------------- /stories/useSize.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useSize } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [sized, state] = useSize(({ width: currentWidth }) => ( 8 |
Size me up! ({currentWidth}px)
9 | )); 10 | 11 | return ( 12 |
13 |
{JSON.stringify(state, null, 2)}
14 | {sized} 15 |
16 | ); 17 | }; 18 | 19 | storiesOf('Sensors/useSize', module) 20 | .add('Docs', () => ) 21 | .add('Demo', () => ); 22 | -------------------------------------------------------------------------------- /stories/useVibrate.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useVibrate, useToggle } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [vibrating, toggleVibrating] = useToggle(false); 8 | 9 | useVibrate(vibrating, [300, 100, 200, 100, 1000, 300]); 10 | 11 | return ( 12 |
13 | 14 |
15 | ); 16 | }; 17 | 18 | storiesOf('UI/useVibrate', module) 19 | .add('Docs', () => ) 20 | .add('Demo', () => ); 21 | -------------------------------------------------------------------------------- /docs/useThrottle.md: -------------------------------------------------------------------------------- 1 | # `useThrottle` and `useThrottleFn` 2 | 3 | React hooks that throttle. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import React, { useState } from 'react'; 9 | import { useThrottle, useThrottleFn } from 'react-use'; 10 | 11 | const Demo = ({value}) => { 12 | const throttledValue = useThrottle(value); 13 | // const throttledValue = useThrottleFn(value => value, 200, [value]); 14 | 15 | return ( 16 | <> 17 |
Value: {value}
18 |
Throttled value: {throttledValue}
19 | 20 | ); 21 | }; 22 | ``` 23 | 24 | ## Reference 25 | 26 | ```ts 27 | useThrottle(value, ms?: number); 28 | useThrottleFn(fn, ms, args); 29 | ``` 30 | -------------------------------------------------------------------------------- /tests/useError.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react-hooks'; 2 | import { useError } from '../src'; 3 | 4 | const setup = () => renderHook(() => useError()); 5 | 6 | beforeEach(() => { 7 | jest.spyOn(console, 'error').mockImplementation(() => {}); 8 | }); 9 | 10 | afterEach(() => { 11 | jest.clearAllMocks(); 12 | }); 13 | 14 | it('should throw an error on error dispatch', () => { 15 | const errorStr = 'some_error'; 16 | 17 | try { 18 | const { result } = setup(); 19 | 20 | act(() => { 21 | result.current(new Error(errorStr)); 22 | }); 23 | } catch (err) { 24 | expect(err.message).toEqual(errorStr); 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /src/useHarmonicIntervalFn.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { clearHarmonicInterval, setHarmonicInterval } from 'set-harmonic-interval'; 3 | 4 | const useHarmonicIntervalFn = (fn: Function, delay: number | null = 0) => { 5 | const latestCallback = useRef(() => {}); 6 | 7 | useEffect(() => { 8 | latestCallback.current = fn; 9 | }); 10 | 11 | useEffect(() => { 12 | if (delay !== null) { 13 | const interval = setHarmonicInterval(() => latestCallback.current(), delay); 14 | return () => clearHarmonicInterval(interval); 15 | } 16 | return undefined; 17 | }, [delay]); 18 | }; 19 | 20 | export default useHarmonicIntervalFn; 21 | -------------------------------------------------------------------------------- /stories/useGetSetState.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useGetSetState } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [get, setState] = useGetSetState<{ cnt: number }>({ cnt: 0 }); 8 | const onClick = () => { 9 | setTimeout(() => { 10 | setState({ cnt: get().cnt + 1 }); 11 | }, 1_000); 12 | }; 13 | 14 | return ; 15 | }; 16 | 17 | storiesOf('State/useGetSetState', module) 18 | .add('Docs', () => ) 19 | .add('Demo', () => ); 20 | -------------------------------------------------------------------------------- /stories/useEffectOnce.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useEffectOnce } from '../src'; 4 | import ConsoleStory from './util/ConsoleStory'; 5 | import ShowDocs from './util/ShowDocs'; 6 | 7 | const Demo = () => { 8 | useEffectOnce(() => { 9 | console.log('Running effect once on mount'); 10 | 11 | return () => { 12 | console.log('Running clean-up of effect on unmount'); 13 | }; 14 | }); 15 | 16 | return ; 17 | }; 18 | 19 | storiesOf('Lifecycle/useEffectOnce', module) 20 | .add('Docs', () => ) 21 | .add('Demo', () => ); 22 | -------------------------------------------------------------------------------- /src/usePageLeave.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { off, on } from './misc/util'; 3 | 4 | const usePageLeave = (onPageLeave, args = []) => { 5 | useEffect(() => { 6 | if (!onPageLeave) { 7 | return; 8 | } 9 | 10 | const handler = (event) => { 11 | event = event ? event : (window.event as any); 12 | const from = event.relatedTarget || event.toElement; 13 | if (!from || (from as any).nodeName === 'HTML') { 14 | onPageLeave(); 15 | } 16 | }; 17 | 18 | on(document, 'mouseout', handler); 19 | return () => { 20 | off(document, 'mouseout', handler); 21 | }; 22 | }, args); 23 | }; 24 | 25 | export default usePageLeave; 26 | -------------------------------------------------------------------------------- /stories/useToggle.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useToggle } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [on, toggle] = useToggle(true); 8 | 9 | return ( 10 |
11 |
{on ? 'ON' : 'OFF'}
12 | 13 | 14 | 15 |
16 | ); 17 | }; 18 | 19 | storiesOf('State/useToggle', module) 20 | .add('Docs', () => ) 21 | .add('Demo', () => ); 22 | -------------------------------------------------------------------------------- /src/usePromise.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import useMountedState from './useMountedState'; 3 | 4 | export type UsePromise = () => (promise: Promise) => Promise; 5 | 6 | const usePromise: UsePromise = () => { 7 | const isMounted = useMountedState(); 8 | return useCallback( 9 | (promise: Promise) => 10 | new Promise((resolve, reject) => { 11 | const onValue = (value) => { 12 | isMounted() && resolve(value); 13 | }; 14 | const onError = (error) => { 15 | isMounted() && reject(error); 16 | }; 17 | promise.then(onValue, onError); 18 | }), 19 | [] 20 | ); 21 | }; 22 | 23 | export default usePromise; 24 | -------------------------------------------------------------------------------- /stories/useSessionStorage.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useSessionStorage } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [value, setValue] = useSessionStorage('hello-key', 'foo'); 8 | 9 | return ( 10 |
11 |
Value: {value}
12 | 13 | 14 |
15 | ); 16 | }; 17 | 18 | storiesOf('Side effects/useSessionStorage', module) 19 | .add('Docs', () => ) 20 | .add('Demo', () => ); 21 | -------------------------------------------------------------------------------- /docs/useError.md: -------------------------------------------------------------------------------- 1 | # `useError` 2 | 3 | React side-effect hook that returns an error dispatcher. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import { useError } from 'react-use'; 9 | 10 | const Demo = () => { 11 | const dispatchError = useError(); 12 | 13 | const clickHandler = () => { 14 | dispatchError(new Error('Some error!')); 15 | }; 16 | 17 | return ; 18 | }; 19 | 20 | // In parent app 21 | const App = () => ( 22 | 23 | 24 | 25 | ); 26 | ``` 27 | 28 | ## Reference 29 | 30 | ```js 31 | const dispatchError = useError(); 32 | ``` 33 | 34 | - `dispatchError` — Callback of type `(err: Error) => void` 35 | -------------------------------------------------------------------------------- /docs/useSlider.md: -------------------------------------------------------------------------------- 1 | # `useSlider` 2 | 3 | React UI hook that provides slide behavior over any HTML element. Supports both mouse and touch events. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useSlider} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const ref = React.useRef(null); 12 | const {isSliding, value, pos, length} = useSlider(ref); 13 | 14 | return ( 15 |
16 |
17 |

18 | {Math.round(state.value * 100)}% 19 |

20 |
🎚
21 |
22 |
23 | ); 24 | }; 25 | ``` 26 | -------------------------------------------------------------------------------- /stories/useBoolean.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useBoolean } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [on, toggle] = useBoolean(true); 8 | 9 | return ( 10 |
11 |
{on ? 'ON' : 'OFF'}
12 | 13 | 14 | 15 |
16 | ); 17 | }; 18 | 19 | storiesOf('State/useBoolean', module) 20 | .add('Docs', () => ) 21 | .add('Demo', () => ); 22 | -------------------------------------------------------------------------------- /stories/useSpring.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import useSpring from '../src/useSpring'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [target, setTarget] = (React as any).useState(50); 8 | const value = useSpring(target); 9 | 10 | return ( 11 |
12 | {value} 13 |
14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | storiesOf('Animation/useSpring', module) 21 | .add('Docs', () => ) 22 | .add('Demo', () => ); 23 | -------------------------------------------------------------------------------- /docs/useClickAway.md: -------------------------------------------------------------------------------- 1 | # `useClickAway` 2 | 3 | React UI hook that triggers a callback when user 4 | clicks outside the target element. 5 | 6 | 7 | ## Usage 8 | 9 | ```jsx 10 | import {useClickAway} from 'react-use'; 11 | 12 | const Demo = () => { 13 | const ref = useRef(null); 14 | useClickAway(ref, () => { 15 | console.log('OUTSIDE CLICKED'); 16 | }); 17 | 18 | return ( 19 |
24 | ); 25 | }; 26 | ``` 27 | 28 | ## Reference 29 | 30 | ```js 31 | useClickAway(ref, onMouseEvent) 32 | useClickAway(ref, onMouseEvent, ['click']) 33 | useClickAway(ref, onMouseEvent, ['mousedown', 'touchstart']) 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/useDeepCompareEffect.md: -------------------------------------------------------------------------------- 1 | # `useDeepCompareEffect` 2 | 3 | A modified useEffect hook that is using deep comparison on its dependencies instead of reference equality. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useCounter, useDeepCompareEffect} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const [count, {inc: inc}] = useCounter(0); 12 | const options = { step: 2 }; 13 | 14 | useDeepCompareEffect(() => { 15 | inc(options.step) 16 | }, [options]); 17 | 18 | return ( 19 |
20 |

useDeepCompareEffect: {count}

21 |
22 | ); 23 | }; 24 | ``` 25 | 26 | ## Reference 27 | 28 | ```ts 29 | useDeepCompareEffect(effect: () => void | (() => void | undefined), deps: any[]); 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/useRaf.md: -------------------------------------------------------------------------------- 1 | # `useRaf` 2 | 3 | React animation hook that forces component to re-render on each `requestAnimationFrame`, 4 | returns percentage of time elapsed. 5 | 6 | 7 | ## Usage 8 | 9 | ```jsx 10 | import {useRaf} from 'react-use'; 11 | 12 | const Demo = () => { 13 | const elapsed = useRaf(5000, 1000); 14 | 15 | return ( 16 |
17 | Elapsed: {elapsed} 18 |
19 | ); 20 | }; 21 | ``` 22 | 23 | 24 | ## Reference 25 | 26 | ```ts 27 | useRaf(ms?: number, delay?: number): number; 28 | ``` 29 | 30 | - `ms` — milliseconds for how long to keep re-rendering component, defaults to `1e12`. 31 | - `delay` — delay in milliseconds after which to start re-rendering component, defaults to `0`. 32 | -------------------------------------------------------------------------------- /src/useGetSet.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, useMemo, useRef } from 'react'; 2 | import useUpdate from './useUpdate'; 3 | import { IHookStateInitAction, IHookStateSetAction, resolveHookState } from './misc/hookState'; 4 | 5 | export default function useGetSet( 6 | initialState: IHookStateInitAction 7 | ): [get: () => S, set: Dispatch>] { 8 | const state = useRef(resolveHookState(initialState)); 9 | const update = useUpdate(); 10 | 11 | return useMemo( 12 | () => [ 13 | () => state.current as S, 14 | (newState: IHookStateSetAction) => { 15 | state.current = resolveHookState(newState, state.current); 16 | update(); 17 | }, 18 | ], 19 | [] 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /docs/useFirstMountState.md: -------------------------------------------------------------------------------- 1 | # `useFirstMountState` 2 | 3 | Returns `true` if component is just mounted (on first render) and `false` otherwise. 4 | 5 | ## Usage 6 | 7 | ```typescript jsx 8 | import * as React from 'react'; 9 | import { useFirstMountState } from 'react-use'; 10 | 11 | const Demo = () => { 12 | const isFirstMount = useFirstMountState(); 13 | const update = useUpdate(); 14 | 15 | return ( 16 |
17 | This component is just mounted: {isFirstMount ? 'YES' : 'NO'} 18 |
19 | 20 |
21 | ); 22 | }; 23 | ``` 24 | 25 | ## Reference 26 | 27 | ```typescript 28 | const isFirstMount: boolean = useFirstMountState(); 29 | ``` 30 | -------------------------------------------------------------------------------- /src/usePreviousDistinct.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { useFirstMountState } from './useFirstMountState'; 3 | 4 | export type Predicate = (prev: T | undefined, next: T) => boolean; 5 | 6 | const strictEquals = (prev: T | undefined, next: T) => prev === next; 7 | 8 | export default function usePreviousDistinct( 9 | value: T, 10 | compare: Predicate = strictEquals 11 | ): T | undefined { 12 | const prevRef = useRef(); 13 | const curRef = useRef(value); 14 | const isFirstMount = useFirstMountState(); 15 | 16 | if (!isFirstMount && !compare(curRef.current, value)) { 17 | prevRef.current = curRef.current; 18 | curRef.current = value; 19 | } 20 | 21 | return prevRef.current; 22 | } 23 | -------------------------------------------------------------------------------- /stories/createMemo.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { createMemo } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const fibonacci = (n) => { 7 | if (n === 0) { 8 | return 0; 9 | } 10 | if (n === 1) { 11 | return 1; 12 | } 13 | return fibonacci(n - 1) + fibonacci(n - 2); 14 | }; 15 | 16 | const useMemoFibonacci = createMemo(fibonacci); 17 | 18 | const Demo = () => { 19 | const result = useMemoFibonacci(10); 20 | 21 | return
fib(10) = {result}
; 22 | }; 23 | 24 | storiesOf('State/createMemo', module) 25 | .add('Docs', () => ) 26 | .add('Demo', () => ); 27 | -------------------------------------------------------------------------------- /stories/useRendersCount.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useRendersCount } from '../src/useRendersCount'; 4 | import useUpdate from '../src/useUpdate'; 5 | import ShowDocs from './util/ShowDocs'; 6 | 7 | const Demo = () => { 8 | const update = useUpdate(); 9 | const rendersCount = useRendersCount(); 10 | 11 | return ( 12 |
13 | Renders count: {rendersCount} 14 |
15 | 16 |
17 | ); 18 | }; 19 | 20 | storiesOf('State/useRendersCount', module) 21 | .add('Docs', () => ) 22 | .add('Demo', () => ); 23 | -------------------------------------------------------------------------------- /tests/useUpdateEffect.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import { useUpdateEffect } from '../src'; 3 | 4 | it('should run effect on update', () => { 5 | const effect = jest.fn(); 6 | 7 | const { rerender } = renderHook(() => useUpdateEffect(effect)); 8 | expect(effect).not.toHaveBeenCalled(); 9 | 10 | rerender(); 11 | expect(effect).toHaveBeenCalledTimes(1); 12 | }); 13 | 14 | it('should run cleanup on unmount', () => { 15 | const cleanup = jest.fn(); 16 | const effect = jest.fn().mockReturnValue(cleanup); 17 | const hook = renderHook(() => useUpdateEffect(effect)); 18 | 19 | hook.rerender(); 20 | hook.unmount(); 21 | 22 | expect(cleanup).toHaveBeenCalledTimes(1); 23 | }); 24 | -------------------------------------------------------------------------------- /stories/useLocation.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useLocation } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const go = (page) => window.history.pushState({}, '', page); 7 | 8 | const Demo = () => { 9 | const state = useLocation(); 10 | 11 | return ( 12 |
13 | 14 | 15 |
{JSON.stringify(state, null, 2)}
16 |
17 | ); 18 | }; 19 | 20 | storiesOf('Sensors/useLocation', module) 21 | .add('Docs', () => ) 22 | .add('Demo', () => ); 23 | -------------------------------------------------------------------------------- /src/useRafState.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react'; 2 | 3 | import useUnmount from './useUnmount'; 4 | 5 | const useRafState = (initialState: S | (() => S)): [S, Dispatch>] => { 6 | const frame = useRef(0); 7 | const [state, setState] = useState(initialState); 8 | 9 | const setRafState = useCallback((value: S | ((prevState: S) => S)) => { 10 | cancelAnimationFrame(frame.current); 11 | 12 | frame.current = requestAnimationFrame(() => { 13 | setState(value); 14 | }); 15 | }, []); 16 | 17 | useUnmount(() => { 18 | cancelAnimationFrame(frame.current); 19 | }); 20 | 21 | return [state, setRafState]; 22 | }; 23 | 24 | export default useRafState; 25 | -------------------------------------------------------------------------------- /stories/useLongPress.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useLongPress } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const onLongPress = () => { 8 | console.log('calls callback after long pressing 300ms'); 9 | }; 10 | 11 | const defaultOptions = { 12 | isPreventDefault: true, 13 | delay: 300, 14 | }; 15 | const longPressEvent = useLongPress(onLongPress, defaultOptions); 16 | 17 | return ; 18 | }; 19 | 20 | storiesOf('Sensors/useLongPress', module) 21 | .add('Docs', () => ) 22 | .add('Demo', () => ); 23 | -------------------------------------------------------------------------------- /tests/usePrevious.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import usePrevious from '../src/usePrevious'; 3 | 4 | const setUp = () => renderHook(({ state }) => usePrevious(state), { initialProps: { state: 0 } }); 5 | 6 | it('should return undefined on initial render', () => { 7 | const { result } = setUp(); 8 | 9 | expect(result.current).toBeUndefined(); 10 | }); 11 | 12 | it('should always return previous state after each update', () => { 13 | const { result, rerender } = setUp(); 14 | 15 | rerender({ state: 2 }); 16 | expect(result.current).toBe(0); 17 | 18 | rerender({ state: 4 }); 19 | expect(result.current).toBe(2); 20 | 21 | rerender({ state: 6 }); 22 | expect(result.current).toBe(4); 23 | }); 24 | -------------------------------------------------------------------------------- /docs/useShallowCompareEffect.md: -------------------------------------------------------------------------------- 1 | # `useShallowCompareEffect` 2 | 3 | A modified useEffect hook that is using shallow comparison on each of its dependencies instead of reference equality. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useCounter, useShallowCompareEffect} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const [count, {inc: inc}] = useCounter(0); 12 | const options = { step: 2 }; 13 | 14 | useShallowCompareEffect(() => { 15 | inc(options.step) 16 | }, [options]); 17 | 18 | return ( 19 |
20 |

useShallowCompareEffect: {count}

21 |
22 | ); 23 | }; 24 | ``` 25 | 26 | ## Reference 27 | 28 | ```ts 29 | useShallowCompareEffect(effect: () => void | (() => void | undefined), deps: any[]); 30 | ``` 31 | -------------------------------------------------------------------------------- /tests/useRendersCount.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import { useRendersCount } from '../src'; 3 | 4 | describe('useRendersCount', () => { 5 | it('should be defined', () => { 6 | expect(useRendersCount).toBeDefined(); 7 | }); 8 | 9 | it('should return number', () => { 10 | expect(renderHook(() => useRendersCount()).result.current).toEqual(expect.any(Number)); 11 | }); 12 | 13 | it('should return actual number of renders', () => { 14 | const hook = renderHook(() => useRendersCount()); 15 | 16 | expect(hook.result.current).toBe(1); 17 | hook.rerender(); 18 | expect(hook.result.current).toBe(2); 19 | hook.rerender(); 20 | expect(hook.result.current).toBe(3); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /docs/useScrollbarWidth.md: -------------------------------------------------------------------------------- 1 | # `useScrollbarWidth` 2 | 3 | Hook that will return current browser's scrollbar width. 4 | In case hook been called before DOM ready, it will return `undefined` and will cause re-render on first available RAF. 5 | > **_NOTE:_** it does not work (return 0) for mobile devices, because their scrollbar width can not be determined. 6 | 7 | ## Usage 8 | 9 | ```jsx 10 | const Demo = () => { 11 | const sbw = useScrollbarWidth(); 12 | 13 | return ( 14 |
15 | {sbw === undefined ? `DOM is not ready yet, SBW detection delayed` : `Browser's scrollbar width is ${sbw}px`} 16 |
17 | ); 18 | }; 19 | ``` 20 | 21 | ## Reference 22 | 23 | ```typescript 24 | const sbw: number | undefined = useScrollbarWidth(); 25 | ``` 26 | -------------------------------------------------------------------------------- /stories/useQueue.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useQueue } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const { add, remove, first, last, size } = useQueue(); 8 | return ( 9 |
10 |
    11 |
  • first: {first}
  • 12 |
  • last: {last}
  • 13 |
  • size: {size}
  • 14 |
15 | 16 | 17 |
18 | ); 19 | }; 20 | 21 | storiesOf('State/useQueue', module) 22 | .add('Docs', () => ) 23 | .add('Demo', () => ); 24 | -------------------------------------------------------------------------------- /docs/useUnmountPromise.md: -------------------------------------------------------------------------------- 1 | # `useUnmountPromise` 2 | 3 | A life-cycle hook that provides a higher order promise that does not resolve if component un-mounts. 4 | 5 | 6 | ## Usage 7 | 8 | ```ts 9 | import useUnmountPromise from 'react-use/lib/useUnmountPromise'; 10 | 11 | const Demo = () => { 12 | const mounted = useUnmountPromise(); 13 | useEffect(async () => { 14 | await mounted(someFunction()); // Will not resolve if component un-mounts. 15 | }); 16 | }; 17 | ``` 18 | 19 | 20 | ## Reference 21 | 22 | ```ts 23 | const mounted = useUnmountPromise(); 24 | 25 | mounted(promise); 26 | mounted(promise, onError); 27 | ``` 28 | 29 | - `onError` — if promise rejects after the component is unmounted, `onError` 30 | callback is called with the error. 31 | -------------------------------------------------------------------------------- /docs/useAsync.md: -------------------------------------------------------------------------------- 1 | # `useAsync` 2 | 3 | React hook that resolves an `async` function or a function that returns 4 | a promise; 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import {useAsync} from 'react-use'; 10 | 11 | const Demo = ({url}) => { 12 | const state = useAsync(async () => { 13 | const response = await fetch(url); 14 | const result = await response.text(); 15 | return result 16 | }, [url]); 17 | 18 | return ( 19 |
20 | {state.loading 21 | ?
Loading...
22 | : state.error 23 | ?
Error: {state.error.message}
24 | :
Value: {state.value}
25 | } 26 |
27 | ); 28 | }; 29 | ``` 30 | 31 | ## Reference 32 | 33 | ```ts 34 | useAsync(fn, args?: any[]); 35 | ``` 36 | -------------------------------------------------------------------------------- /stories/useClickAway.story.tsx: -------------------------------------------------------------------------------- 1 | import { action } from '@storybook/addon-actions'; 2 | import { storiesOf } from '@storybook/react'; 3 | import * as React from 'react'; 4 | import { useRef } from 'react'; 5 | import { useClickAway } from '../src'; 6 | import ShowDocs from './util/ShowDocs'; 7 | 8 | const Demo = () => { 9 | const ref = useRef(null); 10 | useClickAway(ref, action('outside clicked')); 11 | 12 | return ( 13 |
21 | ); 22 | }; 23 | 24 | storiesOf('UI/useClickAway', module) 25 | .add('Docs', () => ) 26 | .add('Demo', () => ); 27 | -------------------------------------------------------------------------------- /docs/createMemo.md: -------------------------------------------------------------------------------- 1 | # `createMemo` 2 | 3 | Hook factory, receives a function to be memoized, returns a memoized React hook, 4 | which receives the same arguments and returns the same result as the original function. 5 | 6 | 7 | ## Usage 8 | 9 | ```jsx 10 | import {createMemo} from 'react-use'; 11 | 12 | const fibonacci = n => { 13 | if (n === 0) return 0; 14 | if (n === 1) return 1; 15 | return fibonacci(n - 1) + fibonacci(n - 2); 16 | }; 17 | 18 | const useMemoFibonacci = createMemo(fibonacci); 19 | 20 | const Demo = () => { 21 | const result = useMemoFibonacci(10); 22 | 23 | return ( 24 |
25 | fib(10) = {result} 26 |
27 | ); 28 | }; 29 | ``` 30 | 31 | 32 | ## Reference 33 | 34 | ```js 35 | const useMemoFn = createMemo(fn); 36 | ``` 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report if you having any problems using the package 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What is the current behavior?** 11 | 12 | **Steps to reproduce it and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have extra dependencies other than `react-use`. Paste the link to your [JSFiddle](https://jsfiddle.net) or [CodeSandbox](https://codesandbox.io) example below:** 13 | 14 | **What is the expected behavior?** 15 | 16 | **A little about versions:** 17 | - _OS_: 18 | - _Browser (vendor and version)_: 19 | - _React_: 20 | - _`react-use`_: 21 | - _Did this worked in the previous package version?_ 22 | -------------------------------------------------------------------------------- /src/useTween.ts: -------------------------------------------------------------------------------- 1 | import { easing } from 'ts-easing'; 2 | import useRaf from './useRaf'; 3 | 4 | export type Easing = (t: number) => number; 5 | 6 | const useTween = (easingName: string = 'inCirc', ms: number = 200, delay: number = 0): number => { 7 | const fn: Easing = easing[easingName]; 8 | const t = useRaf(ms, delay); 9 | 10 | if (process.env.NODE_ENV !== 'production') { 11 | if (typeof fn !== 'function') { 12 | console.error( 13 | 'useTween() expected "easingName" property to be a valid easing function name, like:' + 14 | '"' + 15 | Object.keys(easing).join('", "') + 16 | '".' 17 | ); 18 | console.trace(); 19 | return 0; 20 | } 21 | } 22 | 23 | return fn(t); 24 | }; 25 | 26 | export default useTween; 27 | -------------------------------------------------------------------------------- /stories/useFirstMountState.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useFirstMountState } from '../src/useFirstMountState'; 4 | import useUpdate from '../src/useUpdate'; 5 | import ShowDocs from './util/ShowDocs'; 6 | 7 | const Demo = () => { 8 | const isFirstMount = useFirstMountState(); 9 | const update = useUpdate(); 10 | 11 | return ( 12 |
13 | This component is just mounted: {isFirstMount ? 'YES' : 'NO'} 14 |
15 | 16 |
17 | ); 18 | }; 19 | 20 | storiesOf('State/useFirstMountState', module) 21 | .add('Docs', () => ) 22 | .add('Demo', () => ); 23 | -------------------------------------------------------------------------------- /stories/useNetwork.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useEffect } from 'react'; 4 | import { useNetworkState } from '../src'; 5 | import ShowDocs from './util/ShowDocs'; 6 | 7 | const Demo = () => { 8 | const state = useNetworkState(); 9 | 10 | useEffect(() => { 11 | console.log(state); 12 | }, [state]); 13 | 14 | return ( 15 |
16 |
Since JSON do not output `undefined` fields look the console to see whole the state
17 |
{JSON.stringify(state, null, 2)}
18 |
19 | ); 20 | }; 21 | 22 | storiesOf('Sensors/useNetworkState', module) 23 | .add('Docs', () => ) 24 | .add('Demo', () => ); 25 | -------------------------------------------------------------------------------- /stories/usePrevious.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { usePrevious } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [count, setCount] = React.useState(0); 8 | const prevCount = usePrevious(count); 9 | 10 | return ( 11 |
12 |

13 | Now: {count}, before: {String(prevCount)} 14 |

15 | 16 | 17 |
18 | ); 19 | }; 20 | 21 | storiesOf('State/usePrevious', module) 22 | .add('Docs', () => ) 23 | .add('Demo', () => ); 24 | -------------------------------------------------------------------------------- /src/useTitle.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export interface UseTitleOptions { 4 | restoreOnUnmount?: boolean; 5 | } 6 | 7 | const DEFAULT_USE_TITLE_OPTIONS: UseTitleOptions = { 8 | restoreOnUnmount: false, 9 | }; 10 | 11 | function useTitle(title: string, options: UseTitleOptions = DEFAULT_USE_TITLE_OPTIONS) { 12 | const prevTitleRef = useRef(document.title); 13 | 14 | if (document.title !== title) document.title = title; 15 | 16 | useEffect(() => { 17 | if (options && options.restoreOnUnmount) { 18 | return () => { 19 | document.title = prevTitleRef.current; 20 | }; 21 | } else { 22 | return; 23 | } 24 | }, []); 25 | } 26 | 27 | export default typeof document !== 'undefined' ? useTitle : (_title: string) => {}; 28 | -------------------------------------------------------------------------------- /stories/useIdle.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useIdle } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [idleDelay, setIdleDelay] = React.useState(3e3); 8 | const isIdle = useIdle(idleDelay); 9 | 10 | return ( 11 |
12 | Idle delay ms:{' '} 13 | setIdleDelay(+target.value)} 17 | /> 18 |
User is idle: {isIdle ? 'Yes' : 'No'}
19 |
20 | ); 21 | }; 22 | 23 | storiesOf('Sensors/useIdle', module) 24 | .add('Docs', () => ) 25 | .add('Demo', () => ); 26 | -------------------------------------------------------------------------------- /docs/usePrevious.md: -------------------------------------------------------------------------------- 1 | # `usePrevious` 2 | 3 | React state hook that returns the previous state as described in the [React hooks FAQ](https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state). 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {usePrevious} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const [count, setCount] = React.useState(0); 12 | const prevCount = usePrevious(count); 13 | 14 | return ( 15 |

16 | 17 | 18 |

19 | Now: {count}, before: {prevCount} 20 |

21 |

22 | ); 23 | }; 24 | ``` 25 | 26 | ## Reference 27 | 28 | ```ts 29 | const prevState = usePrevious = (state: T): T; 30 | ``` 31 | -------------------------------------------------------------------------------- /src/useUpsert.ts: -------------------------------------------------------------------------------- 1 | import useList, { ListActions } from './useList'; 2 | import { IHookStateInitAction } from './misc/hookState'; 3 | 4 | export interface UpsertListActions extends Omit, 'upsert'> { 5 | upsert: (newItem: T) => void; 6 | } 7 | 8 | /** 9 | * @deprecated Use `useList` hook's upsert action instead 10 | */ 11 | export default function useUpsert( 12 | predicate: (a: T, b: T) => boolean, 13 | initialList: IHookStateInitAction = [] 14 | ): [T[], UpsertListActions] { 15 | const [list, listActions] = useList(initialList); 16 | 17 | return [ 18 | list, 19 | { 20 | ...listActions, 21 | upsert: (newItem: T) => { 22 | listActions.upsert(predicate, newItem); 23 | }, 24 | } as UpsertListActions, 25 | ]; 26 | } 27 | -------------------------------------------------------------------------------- /stories/useDefault.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useDefault } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const initialUser = { name: 'Marshall' }; 8 | const defaultUser = { name: 'Mathers' }; 9 | const [user, setUser] = useDefault(defaultUser, initialUser); 10 | 11 | return ( 12 |
13 |
User: {user.name}
14 | setUser({ name: e.target.value })} /> 15 | 16 |
17 | ); 18 | }; 19 | 20 | storiesOf('State/useDefault', module) 21 | .add('Docs', () => ) 22 | .add('Demo', () => ); 23 | -------------------------------------------------------------------------------- /docs/useVibrate.md: -------------------------------------------------------------------------------- 1 | # `useVibrate` 2 | 3 | React UI hook to provide physical feedback with device vibration hardware using the [Vibration API](https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API). 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useVibrate} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const [vibrating, toggleVibrating] = useToggle(false); 12 | 13 | useVibrate(vibrating, [300, 100, 200, 100, 1000, 300], false); 14 | 15 | return ( 16 |
17 | 18 |
19 | ); 20 | }; 21 | ``` 22 | 23 | ## Reference 24 | 25 | ```ts 26 | useVibrate( 27 | enabled: boolean = true, 28 | pattern: number | number[] = [1000, 1000], 29 | loop: boolean = true 30 | ): void; 31 | ``` 32 | -------------------------------------------------------------------------------- /tests/useNetworkState.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import { useNetworkState } from '../src'; 3 | 4 | //ToDo: improve tests 5 | describe(`useNetworkState`, () => { 6 | it('should be defined', () => { 7 | expect(useNetworkState).toBeDefined(); 8 | }); 9 | 10 | it('should return an object of certain structure', () => { 11 | const hook = renderHook(() => useNetworkState(), { initialProps: false }); 12 | 13 | expect(typeof hook.result.current).toEqual('object'); 14 | expect(Object.keys(hook.result.current)).toEqual([ 15 | 'online', 16 | 'previous', 17 | 'since', 18 | 'downlink', 19 | 'downlinkMax', 20 | 'effectiveType', 21 | 'rtt', 22 | 'saveData', 23 | 'type', 24 | ]); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /docs/useMountedState.md: -------------------------------------------------------------------------------- 1 | # `useMountedState` 2 | 3 | > **NOTE!:** despite having `State` in its name **_this hook does not cause component re-render_**. 4 | > This component designed to be used to avoid state updates on unmounted components. 5 | 6 | Lifecycle hook providing ability to check component's mount state. 7 | Returns a function that will return `true` if component mounted and `false` otherwise. 8 | 9 | ## Usage 10 | 11 | ```jsx 12 | import * as React from 'react'; 13 | import {useMountedState} from 'react-use'; 14 | 15 | const Demo = () => { 16 | const isMounted = useMountedState(); 17 | 18 | React.useEffect(() => { 19 | setTimeout(() => { 20 | if (isMounted()) { 21 | // ... 22 | } else { 23 | // ... 24 | } 25 | }, 1000); 26 | }); 27 | }; 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/useSet.md: -------------------------------------------------------------------------------- 1 | # `useSet` 2 | 3 | React state hook that tracks a [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set). 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useSet} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const [set, { add, has, remove, toggle, reset }] = useSet(new Set(['hello'])); 12 | 13 | return ( 14 |
15 | 16 | 17 | 20 | 21 |
{JSON.stringify(Array.from(set), null, 2)}
22 |
23 | ); 24 | }; 25 | ``` 26 | -------------------------------------------------------------------------------- /tests/useFirstMountState.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import { useFirstMountState } from '../src'; 3 | 4 | describe('useFirstMountState', () => { 5 | it('should be defined', () => { 6 | expect(useFirstMountState).toBeDefined(); 7 | }); 8 | 9 | it('should return boolean', () => { 10 | expect(renderHook(() => useFirstMountState()).result.current).toEqual(expect.any(Boolean)); 11 | }); 12 | 13 | it('should return true on first render and false on all others', () => { 14 | const hook = renderHook(() => useFirstMountState()); 15 | 16 | expect(hook.result.current).toBe(true); 17 | hook.rerender(); 18 | expect(hook.result.current).toBe(false); 19 | hook.rerender(); 20 | expect(hook.result.current).toBe(false); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /stories/useUpdateEffect.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useUpdateEffect } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [count, setCount] = React.useState(0); 8 | const [didUpdate, setDidUpdate] = React.useState(false); 9 | 10 | useUpdateEffect(() => { 11 | setDidUpdate(true); 12 | }, [count]); 13 | 14 | return ( 15 |
16 | 17 |

Updated: {didUpdate}

18 |
19 | ); 20 | }; 21 | 22 | storiesOf('Lifecycle/useUpdateEffect', module) 23 | .add('Docs', () => ) 24 | .add('Demo', () => ); 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "jsx": "react", 7 | "declaration": true, 8 | "pretty": true, 9 | "rootDir": "src", 10 | "sourceMap": false, 11 | "strict": true, 12 | "esModuleInterop": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noImplicitAny": false, 17 | "noFallthroughCasesInSwitch": true, 18 | "outDir": "lib", 19 | "lib": [ 20 | "es2018", 21 | "dom" 22 | ], 23 | "importHelpers": true 24 | }, 25 | "exclude": [ 26 | "node_modules", 27 | "lib", 28 | "esm", 29 | "tests", 30 | "stories", 31 | "jest.config.ts", 32 | "jest.config.*.ts" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /src/useObservable.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect'; 3 | 4 | export interface Observable { 5 | subscribe: (listener: (value: T) => void) => { 6 | unsubscribe: () => void; 7 | }; 8 | } 9 | 10 | function useObservable(observable$: Observable): T | undefined; 11 | function useObservable(observable$: Observable, initialValue: T): T; 12 | function useObservable(observable$: Observable, initialValue?: T): T | undefined { 13 | const [value, update] = useState(initialValue); 14 | 15 | useIsomorphicLayoutEffect(() => { 16 | const s = observable$.subscribe(update); 17 | return () => s.unsubscribe(); 18 | }, [observable$]); 19 | 20 | return value; 21 | } 22 | 23 | export default useObservable; 24 | -------------------------------------------------------------------------------- /stories/useWindowScroll.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useWindowScroll } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const { x, y } = useWindowScroll(); 8 | 9 | return ( 10 |
15 |
21 |
x: {x}
22 |
y: {y}
23 |
24 |
25 | ); 26 | }; 27 | 28 | storiesOf('Sensors/useWindowScroll', module) 29 | .add('Docs', () => ) 30 | .add('Demo', () => ); 31 | -------------------------------------------------------------------------------- /docs/useSize.md: -------------------------------------------------------------------------------- 1 | # `useSize` 2 | 3 | React sensor hook that tracks size of an HTML element. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useSize} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const [sized, {width, height}] = useSize( 12 | ({width}) =>
Size me up! ({width}px)
, 13 | { width: 100, height: 100 } 14 | ); 15 | 16 | return ( 17 |
18 | {sized} 19 |
width: {width}
20 |
height: {height}
21 |
22 | ); 23 | }; 24 | ``` 25 | 26 | ## Reference 27 | 28 | ```js 29 | useSize(element, initialSize); 30 | ``` 31 | 32 | - `element` — sized element. 33 | - `initialSize` — initial size containing a `width` and `height` key. 34 | 35 | ## Related hooks 36 | 37 | - [useMeasure](./useMeasure.md) 38 | -------------------------------------------------------------------------------- /src/useCookie.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | import Cookies from 'js-cookie'; 3 | 4 | const useCookie = ( 5 | cookieName: string 6 | ): [string | null, (newValue: string, options?: Cookies.CookieAttributes) => void, () => void] => { 7 | const [value, setValue] = useState(() => Cookies.get(cookieName) || null); 8 | 9 | const updateCookie = useCallback( 10 | (newValue: string, options?: Cookies.CookieAttributes) => { 11 | Cookies.set(cookieName, newValue, options); 12 | setValue(newValue); 13 | }, 14 | [cookieName] 15 | ); 16 | 17 | const deleteCookie = useCallback(() => { 18 | Cookies.remove(cookieName); 19 | setValue(null); 20 | }, [cookieName]); 21 | 22 | return [value, updateCookie, deleteCookie]; 23 | }; 24 | 25 | export default useCookie; 26 | -------------------------------------------------------------------------------- /tests/useEffectOnce.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import { useEffectOnce } from '../src'; 3 | 4 | const mockEffectCleanup = jest.fn(); 5 | const mockEffectCallback = jest.fn().mockReturnValue(mockEffectCleanup); 6 | 7 | it('should run provided effect only once', () => { 8 | const { rerender } = renderHook(() => useEffectOnce(mockEffectCallback)); 9 | expect(mockEffectCallback).toHaveBeenCalledTimes(1); 10 | 11 | rerender(); 12 | expect(mockEffectCallback).toHaveBeenCalledTimes(1); 13 | }); 14 | 15 | it('should run clean-up provided on unmount', () => { 16 | const { unmount } = renderHook(() => useEffectOnce(mockEffectCallback)); 17 | expect(mockEffectCleanup).not.toHaveBeenCalled(); 18 | 19 | unmount(); 20 | expect(mockEffectCleanup).toHaveBeenCalledTimes(1); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/useLatest.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import useLatest from '../src/useLatest'; 3 | 4 | const setUp = () => renderHook(({ state }) => useLatest(state), { initialProps: { state: 0 } }); 5 | 6 | it('should return a ref with the latest value on initial render', () => { 7 | const { result } = setUp(); 8 | 9 | expect(result.current).toEqual({ current: 0 }); 10 | }); 11 | 12 | it('should always return a ref with the latest value after each update', () => { 13 | const { result, rerender } = setUp(); 14 | 15 | rerender({ state: 2 }); 16 | expect(result.current).toEqual({ current: 2 }); 17 | 18 | rerender({ state: 4 }); 19 | expect(result.current).toEqual({ current: 4 }); 20 | 21 | rerender({ state: 6 }); 22 | expect(result.current).toEqual({ current: 6 }); 23 | }); 24 | -------------------------------------------------------------------------------- /docs/useFullscreen.md: -------------------------------------------------------------------------------- 1 | # `useFullscreen` 2 | 3 | Display an element full-screen, optional fallback for fullscreen video on iOS. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useFullscreen, useToggle} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const ref = useRef(null) 12 | const [show, toggle] = useToggle(false); 13 | const isFullscreen = useFullscreen(ref, show, {onClose: () => toggle(false)}); 14 | 15 | return ( 16 |
17 |
{isFullscreen ? 'Fullscreen' : 'Not fullscreen'}
18 | 19 |
21 | ); 22 | }; 23 | ``` 24 | 25 | ## Reference 26 | 27 | ```ts 28 | useFullscreen(ref, show, {onClose}) 29 | ``` 30 | -------------------------------------------------------------------------------- /stories/useMap.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useMap } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [map, { set, remove, reset }] = useMap({ 8 | hello: 'there', 9 | }); 10 | 11 | return ( 12 |
13 | 14 | 15 | 18 |
{JSON.stringify(map, null, 2)}
19 |
20 | ); 21 | }; 22 | 23 | storiesOf('State/useMap', module) 24 | .add('Docs', () => ) 25 | .add('Demo', () => ); 26 | -------------------------------------------------------------------------------- /docs/useMap.md: -------------------------------------------------------------------------------- 1 | # `useMap` 2 | 3 | React state hook that tracks a value of an object. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useMap} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const [map, {set, setAll, remove, reset}] = useMap({ 12 | hello: 'there', 13 | }); 14 | 15 | return ( 16 |
17 | 20 | 23 | 26 | 29 |
{JSON.stringify(map, null, 2)}
30 |
31 | ); 32 | }; 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/useRafState.md: -------------------------------------------------------------------------------- 1 | # `useRafState` 2 | 3 | React state hook that only updates state in the callback of [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame). 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useRafState, useMount} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const [state, setState] = useRafState({ 12 | width: 0, 13 | height: 0, 14 | }); 15 | 16 | useMount(() => { 17 | const onResize = () => { 18 | setState({ 19 | width: window.clientWidth, 20 | height: window.height, 21 | }); 22 | }; 23 | 24 | window.addEventListener('resize', onResize); 25 | 26 | return () => { 27 | window.removeEventListener('resize', onResize); 28 | }; 29 | }); 30 | 31 | return
{JSON.stringify(state, null, 2)}
; 32 | }; 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/useEvent.md: -------------------------------------------------------------------------------- 1 | # `useEvent` 2 | 3 | React sensor hook that subscribes a `handler` to events. 4 | 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import {useEvent, useList} from 'react-use'; 10 | 11 | const Demo = () => { 12 | const [list, {push, clear}] = useList(); 13 | 14 | const onKeyDown = useCallback(({key}) => { 15 | if (key === 'r') clear(); 16 | push(key); 17 | }, []); 18 | 19 | useEvent('keydown', onKeyDown); 20 | 21 | return ( 22 |
23 |

24 | Press some keys on your keyboard, r key resets the list 25 |

26 |
27 |         {JSON.stringify(list, null, 4)}
28 |       
29 |
30 | ); 31 | }; 32 | ``` 33 | 34 | 35 | ## Examples 36 | 37 | ```js 38 | useEvent('keydown', handler) 39 | useEvent('scroll', handler, window, {capture: true}) 40 | ``` 41 | -------------------------------------------------------------------------------- /src/useQueue.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export interface QueueMethods { 4 | add: (item: T) => void; 5 | remove: () => T; 6 | first: T; 7 | last: T; 8 | size: number; 9 | } 10 | 11 | const useQueue = (initialValue: T[] = []): QueueMethods => { 12 | const [state, set] = useState(initialValue); 13 | return { 14 | add: (value) => { 15 | set((queue) => [...queue, value]); 16 | }, 17 | remove: () => { 18 | let result; 19 | set(([first, ...rest]) => { 20 | result = first; 21 | return rest; 22 | }); 23 | return result; 24 | }, 25 | get first() { 26 | return state[0]; 27 | }, 28 | get last() { 29 | return state[state.length - 1]; 30 | }, 31 | get size() { 32 | return state.length; 33 | }, 34 | }; 35 | }; 36 | 37 | export default useQueue; 38 | -------------------------------------------------------------------------------- /stories/useHash.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useHash, useMount } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [hash, setHash] = useHash(); 8 | 9 | useMount(() => { 10 | setHash('#/path/to/page?userId=123'); 11 | }); 12 | 13 | return ( 14 |
15 |
window.location.href:
16 |
17 |
{window.location.href}
18 |
19 |
Edit hash:
20 |
21 | setHash(e.target.value)} /> 22 |
23 |
24 | ); 25 | }; 26 | 27 | storiesOf('Sensors/useHash', module) 28 | .add('Docs', () => ) 29 | .add('Demo', () => ); 30 | -------------------------------------------------------------------------------- /docs/useKeyPress.md: -------------------------------------------------------------------------------- 1 | # `useKeyPress` 2 | 3 | React UI sensor hook that detects when the user is pressing a specific 4 | key on their keyboard. 5 | 6 | 7 | ## Usage 8 | 9 | ```jsx 10 | import {useKeyPress} from 'react-use'; 11 | 12 | const keys = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0']; 13 | 14 | const Demo = () => { 15 | const states = []; 16 | for (const key of keys) states.push(useKeyPress(key)[0]); 17 | 18 | return ( 19 |
20 | Try pressing numbers 21 |
22 | {states.reduce((s, pressed, index) => s + (pressed ? (s ? ' + ' : '') + keys[index] : ''), '')} 23 |
24 | ); 25 | }; 26 | ``` 27 | 28 | 29 | ## Examples 30 | 31 | ```js 32 | const isPressed = useKeyPress('a'); 33 | 34 | const predicate = (event) => event.key === 'a'; 35 | const isPressed = useKeyPress(predicate); 36 | ``` 37 | -------------------------------------------------------------------------------- /src/useMouseHovered.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | import useHoverDirty from './useHoverDirty'; 3 | import useMouse, { State } from './useMouse'; 4 | 5 | export interface UseMouseHoveredOptions { 6 | whenHovered?: boolean; 7 | bound?: boolean; 8 | } 9 | 10 | const nullRef = { current: null }; 11 | 12 | const useMouseHovered = (ref: RefObject, options: UseMouseHoveredOptions = {}): State => { 13 | const whenHovered = !!options.whenHovered; 14 | const bound = !!options.bound; 15 | 16 | const isHovered = useHoverDirty(ref, whenHovered); 17 | const state = useMouse(whenHovered && !isHovered ? nullRef : ref); 18 | 19 | if (bound) { 20 | state.elX = Math.max(0, Math.min(state.elX, state.elW)); 21 | state.elY = Math.max(0, Math.min(state.elY, state.elH)); 22 | } 23 | 24 | return state; 25 | }; 26 | 27 | export default useMouseHovered; 28 | -------------------------------------------------------------------------------- /stories/usePreviousDistinct.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { usePreviousDistinct, useCounter } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [count, { inc: relatedInc }] = useCounter(0); 8 | const [unrelatedCount, { inc }] = useCounter(0); 9 | const prevCount = usePreviousDistinct(count); 10 | 11 | return ( 12 |

13 | Now: {count}, before: {prevCount} 14 | 15 | Unrelated: {unrelatedCount} 16 | 17 |

18 | ); 19 | }; 20 | 21 | storiesOf('State/usePreviousDistinct', module) 22 | .add('Docs', () => ) 23 | .add('Demo', () => ); 24 | -------------------------------------------------------------------------------- /docs/useTween.md: -------------------------------------------------------------------------------- 1 | # `useTween` 2 | 3 | React animation hook that tweens a number between 0 and 1. 4 | 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import {useTween} from 'react-use'; 10 | 11 | const Demo = () => { 12 | const t = useTween(); 13 | 14 | return ( 15 |
16 | Tween: {t} 17 |
18 | ); 19 | }; 20 | ``` 21 | 22 | ## Reference 23 | 24 | ```ts 25 | useTween(easing?: string, ms?: number, delay?: number): number 26 | ``` 27 | 28 | Returns a number that begins with 0 and ends with 1 when animation ends. 29 | 30 | - `easing` — one of the valid [easing names](https://github.com/streamich/ts-easing/blob/master/src/index.ts), defaults to `inCirc`. 31 | - `ms` — milliseconds for how long to keep re-rendering component, defaults to `200`. 32 | - `delay` — delay in milliseconds after which to start re-rendering component, defaults to `0`. 33 | -------------------------------------------------------------------------------- /src/useKeyboardJs.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import useMount from './useMount'; 3 | 4 | const useKeyboardJs = (combination: string | string[]) => { 5 | const [state, set] = useState<[boolean, null | KeyboardEvent]>([false, null]); 6 | const [keyboardJs, setKeyboardJs] = useState(null); 7 | 8 | useMount(() => { 9 | import('keyboardjs').then((k) => setKeyboardJs(k.default || k)); 10 | }); 11 | 12 | useEffect(() => { 13 | if (!keyboardJs) { 14 | return; 15 | } 16 | 17 | const down = (event) => set([true, event]); 18 | const up = (event) => set([false, event]); 19 | keyboardJs.bind(combination, down, up, true); 20 | 21 | return () => { 22 | keyboardJs.unbind(combination, down, up); 23 | }; 24 | }, [combination, keyboardJs]); 25 | 26 | return state; 27 | }; 28 | 29 | export default useKeyboardJs; 30 | -------------------------------------------------------------------------------- /docs/useAsyncRetry.md: -------------------------------------------------------------------------------- 1 | # `useAsyncRetry` 2 | 3 | Uses `useAsync` with an additional `retry` method to easily retry/refresh the async function; 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useAsyncRetry} from 'react-use'; 9 | 10 | const Demo = ({url}) => { 11 | const state = useAsyncRetry(async () => { 12 | const response = await fetch(url); 13 | const result = await response.text(); 14 | return result; 15 | }, [url]); 16 | 17 | return ( 18 |
19 | {state.loading 20 | ?
Loading...
21 | : state.error 22 | ?
Error: {state.error.message}
23 | :
Value: {state.value}
24 | } 25 | {!loading && } 26 |
27 | ); 28 | }; 29 | ``` 30 | 31 | ## Reference 32 | 33 | ```ts 34 | useAsyncRetry(fn, args?: any[]); 35 | ``` 36 | -------------------------------------------------------------------------------- /src/misc/util.ts: -------------------------------------------------------------------------------- 1 | export const noop = () => {}; 2 | 3 | export function on( 4 | obj: T | null, 5 | ...args: Parameters | [string, Function | null, ...any] 6 | ): void { 7 | if (obj && obj.addEventListener) { 8 | obj.addEventListener(...(args as Parameters)); 9 | } 10 | } 11 | 12 | export function off( 13 | obj: T | null, 14 | ...args: Parameters | [string, Function | null, ...any] 15 | ): void { 16 | if (obj && obj.removeEventListener) { 17 | obj.removeEventListener(...(args as Parameters)); 18 | } 19 | } 20 | 21 | export const isBrowser = typeof window !== 'undefined'; 22 | 23 | export const isNavigator = typeof navigator !== 'undefined'; 24 | -------------------------------------------------------------------------------- /src/useHash.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | import useLifecycles from './useLifecycles'; 3 | import { off, on } from './misc/util'; 4 | 5 | /** 6 | * read and write url hash, response to url hash change 7 | */ 8 | export const useHash = () => { 9 | const [hash, setHash] = useState(() => window.location.hash); 10 | 11 | const onHashChange = useCallback(() => { 12 | setHash(window.location.hash); 13 | }, []); 14 | 15 | useLifecycles( 16 | () => { 17 | on(window, 'hashchange', onHashChange); 18 | }, 19 | () => { 20 | off(window, 'hashchange', onHashChange); 21 | } 22 | ); 23 | 24 | const _setHash = useCallback( 25 | (newHash: string) => { 26 | if (newHash !== hash) { 27 | window.location.hash = newHash; 28 | } 29 | }, 30 | [hash] 31 | ); 32 | 33 | return [hash, _setHash] as const; 34 | }; 35 | -------------------------------------------------------------------------------- /stories/useScroll.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useScroll } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const scrollRef = React.useRef(null); 8 | const { x, y } = useScroll(scrollRef); 9 | 10 | return ( 11 | <> 12 |
x: {x}
13 |
y: {y}
14 |
22 |
Scroll me
23 |
24 | 25 | ); 26 | }; 27 | 28 | storiesOf('Sensors/useScroll', module) 29 | .add('Docs', () => ) 30 | .add('Demo', () => ); 31 | -------------------------------------------------------------------------------- /stories/useSet.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useSet } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [set, { add, has, remove, reset, toggle }] = useSet(new Set(['hello'])); 8 | 9 | return ( 10 |
11 | 12 | 13 | 16 | 17 |
{JSON.stringify(Array.from(set), null, 2)}
18 |
19 | ); 20 | }; 21 | 22 | storiesOf('State/useSet', module) 23 | .add('Docs', () => ) 24 | .add('Demo', () => ); 25 | -------------------------------------------------------------------------------- /stories/useTimeout.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useTimeout } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | function TestComponent(props: { ms?: number } = {}) { 7 | const ms = props.ms || 5000; 8 | const [isReady, cancel] = useTimeout(ms); 9 | 10 | return ( 11 |
12 | {isReady() ? "I'm reloaded after timeout" : `I will be reloaded after ${ms / 1000}s`} 13 | {isReady() === false ? : ''} 14 |
15 | ); 16 | } 17 | 18 | const Demo = () => { 19 | return ( 20 |
21 | 22 | 23 |
24 | ); 25 | }; 26 | 27 | storiesOf('Animation/useTimeout', module) 28 | .add('Docs', () => ) 29 | .add('Demo', () => ); 30 | -------------------------------------------------------------------------------- /docs/useUpdateEffect.md: -------------------------------------------------------------------------------- 1 | # `useUpdateEffect` 2 | 3 | React effect hook that ignores the first invocation (e.g. on mount). The signature is exactly the same as the `useEffect` hook. 4 | 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import React from 'react' 10 | import {useUpdateEffect} from 'react-use'; 11 | 12 | const Demo = () => { 13 | const [count, setCount] = React.useState(0); 14 | 15 | React.useEffect(() => { 16 | const interval = setInterval(() => { 17 | setCount(count => count + 1) 18 | }, 1000) 19 | 20 | return () => { 21 | clearInterval(interval) 22 | } 23 | }, []) 24 | 25 | useUpdateEffect(() => { 26 | console.log('count', count) // will only show 1 and beyond 27 | 28 | return () => { // *OPTIONAL* 29 | // do something on unmount 30 | } 31 | }) // you can include deps array if necessary 32 | 33 | return
Count: {count}
34 | }; 35 | ``` 36 | -------------------------------------------------------------------------------- /src/useBeforeUnload.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from 'react'; 2 | import { off, on } from './misc/util'; 3 | 4 | const useBeforeUnload = (enabled: boolean | (() => boolean) = true, message?: string) => { 5 | const handler = useCallback( 6 | (event: BeforeUnloadEvent) => { 7 | const finalEnabled = typeof enabled === 'function' ? enabled() : true; 8 | 9 | if (!finalEnabled) { 10 | return; 11 | } 12 | 13 | event.preventDefault(); 14 | 15 | if (message) { 16 | event.returnValue = message; 17 | } 18 | 19 | return message; 20 | }, 21 | [enabled, message] 22 | ); 23 | 24 | useEffect(() => { 25 | if (!enabled) { 26 | return; 27 | } 28 | 29 | on(window, 'beforeunload', handler); 30 | 31 | return () => off(window, 'beforeunload', handler); 32 | }, [enabled, handler]); 33 | }; 34 | 35 | export default useBeforeUnload; 36 | -------------------------------------------------------------------------------- /docs/useCustomCompareEffect.md: -------------------------------------------------------------------------------- 1 | # `useCustomCompareEffect` 2 | 3 | A modified useEffect hook that accepts a comparator which is used for comparison on dependencies instead of reference equality. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useCounter, useCustomCompareEffect} from 'react-use'; 9 | import isEqual from 'lodash/isEqual'; 10 | 11 | const Demo = () => { 12 | const [count, {inc: inc}] = useCounter(0); 13 | const options = { step: 2 }; 14 | 15 | useCustomCompareEffect(() => { 16 | inc(options.step) 17 | }, [options], (prevDeps, nextDeps) => isEqual(prevDeps, nextDeps)); 18 | 19 | return ( 20 |
21 |

useCustomCompareEffect with deep comparison: {count}

22 |
23 | ); 24 | }; 25 | ``` 26 | 27 | ## Reference 28 | 29 | ```ts 30 | useCustomCompareEffect(effect: () => void | (() => void | undefined), deps: any[], depsEqual: (prevDeps: any[], nextDeps: any[]) => boolean); 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/useKey.md: -------------------------------------------------------------------------------- 1 | # `useKey` 2 | 3 | React UI sensor hook that executes a `handler` when a keyboard key is used. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useKey} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const [count, set] = useState(0); 12 | const increment = () => set(count => ++count); 13 | useKey('ArrowUp', increment); 14 | 15 | return ( 16 |
17 | Press arrow up: {count} 18 |
19 | ); 20 | }; 21 | ``` 22 | 23 | Or as render-prop: 24 | 25 | ```jsx 26 | import UseKey from 'react-use/lib/component/UseKey'; 27 | 28 | alert('"a" key pressed!')} /> 29 | ``` 30 | 31 | 32 | ## Reference 33 | 34 | ```js 35 | useKey(filter, handler, options?, deps?) 36 | ``` 37 | 38 | 39 | ## Examples 40 | 41 | ```js 42 | useKey('a', () => alert('"a" pressed')); 43 | 44 | const predicate = (event) => event.key === 'a' 45 | useKey(predicate, handler, {event: 'keyup'}); 46 | ``` 47 | -------------------------------------------------------------------------------- /src/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | import useRafState from './useRafState'; 4 | import { isBrowser, off, on } from './misc/util'; 5 | 6 | const useWindowSize = (initialWidth = Infinity, initialHeight = Infinity) => { 7 | const [state, setState] = useRafState<{ width: number; height: number }>({ 8 | width: isBrowser ? window.innerWidth : initialWidth, 9 | height: isBrowser ? window.innerHeight : initialHeight, 10 | }); 11 | 12 | useEffect((): (() => void) | void => { 13 | if (isBrowser) { 14 | const handler = () => { 15 | setState({ 16 | width: window.innerWidth, 17 | height: window.innerHeight, 18 | }); 19 | }; 20 | 21 | on(window, 'resize', handler); 22 | 23 | return () => { 24 | off(window, 'resize', handler); 25 | }; 26 | } 27 | }, []); 28 | 29 | return state; 30 | }; 31 | 32 | export default useWindowSize; 33 | -------------------------------------------------------------------------------- /stories/useLatest.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useLatest } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [count, setCount] = React.useState(0); 8 | const latestCount = useLatest(count); 9 | const timeoutMs = 3000; 10 | 11 | function handleAlertClick() { 12 | setTimeout(() => { 13 | alert(`Latest count value: ${latestCount.current}`); 14 | }, timeoutMs); 15 | } 16 | 17 | return ( 18 |
19 |

You clicked {count} times

20 | 21 | 22 |
23 | ); 24 | }; 25 | 26 | storiesOf('State/useLatest', module) 27 | .add('Docs', () => ) 28 | .add('Demo', () => ); 29 | -------------------------------------------------------------------------------- /stories/useMediatedState.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useMediatedState } from '../src/useMediatedState'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const inputMediator = (s) => s.replace(/[\s]+/g, ' '); 7 | const Demo = () => { 8 | const [state, setState] = useMediatedState(inputMediator, ''); 9 | 10 | return ( 11 |
12 |
You will not be able to enter more than one space
13 | ) => { 19 | setState(ev.target.value); 20 | }} 21 | /> 22 |
23 | ); 24 | }; 25 | 26 | storiesOf('State/useMediatedState', module) 27 | .add('Docs', () => ) 28 | .add('Demo', () => ); 29 | -------------------------------------------------------------------------------- /stories/useScrolling.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useScrolling } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const scrollRef = React.useRef(null); 8 | const scrolling = useScrolling(scrollRef); 9 | 10 | return ( 11 | <> 12 | {
{scrolling ? 'Scrolling' : 'Not scrolling'}
} 13 |
14 |
22 |
Scroll me
23 |
24 | 25 | ); 26 | }; 27 | 28 | storiesOf('Sensors/useScrolling', module) 29 | .add('Docs', () => ) 30 | .add('Demo', () => ); 31 | -------------------------------------------------------------------------------- /stories/useSetState.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useSetState } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [state, setState] = useSetState({}); 8 | 9 | return ( 10 |
11 |
{JSON.stringify(state, null, 2)}
12 | 13 | 14 | 22 |
23 | ); 24 | }; 25 | 26 | storiesOf('State/useSetState', module) 27 | .add('Docs', () => ) 28 | .add('Demo', () => ); 29 | -------------------------------------------------------------------------------- /src/useDeepCompareEffect.ts: -------------------------------------------------------------------------------- 1 | import { DependencyList, EffectCallback } from 'react'; 2 | import useCustomCompareEffect from './useCustomCompareEffect'; 3 | import isDeepEqual from './misc/isDeepEqual'; 4 | 5 | const isPrimitive = (val: any) => val !== Object(val); 6 | 7 | const useDeepCompareEffect = (effect: EffectCallback, deps: DependencyList) => { 8 | if (process.env.NODE_ENV !== 'production') { 9 | if (!(deps instanceof Array) || !deps.length) { 10 | console.warn( 11 | '`useDeepCompareEffect` should not be used with no dependencies. Use React.useEffect instead.' 12 | ); 13 | } 14 | 15 | if (deps.every(isPrimitive)) { 16 | console.warn( 17 | '`useDeepCompareEffect` should not be used with dependencies that are all primitive values. Use React.useEffect instead.' 18 | ); 19 | } 20 | } 21 | 22 | useCustomCompareEffect(effect, deps, isDeepEqual); 23 | }; 24 | 25 | export default useDeepCompareEffect; 26 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | max_line_length = 100 12 | 13 | [*.{ts, tsx}] 14 | ij_typescript_enforce_trailing_comma = keep 15 | ij_typescript_use_double_quotes = false 16 | ij_typescript_force_quote_style = true 17 | ij_typescript_align_imports = false 18 | ij_typescript_align_multiline_ternary_operation = false 19 | ij_typescript_align_multiline_parameters_in_calls = false 20 | ij_typescript_align_multiline_parameters = false 21 | ij_typescript_align_multiline_chained_methods = false 22 | ij_typescript_else_on_new_line = false 23 | ij_typescript_catch_on_new_line = false 24 | ij_typescript_spaces_within_interpolation_expressions = false 25 | 26 | [*.md] 27 | max_line_length = 100 28 | trim_trailing_whitespace = false 29 | 30 | [COMMIT_EDITMSG] 31 | max_line_length = 80 32 | -------------------------------------------------------------------------------- /tests/useMount.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import { useMount } from '../src'; 3 | 4 | const mockCallback = jest.fn(); 5 | 6 | afterEach(() => { 7 | jest.resetAllMocks(); 8 | }); 9 | 10 | it('should call provided callback on mount', () => { 11 | renderHook(() => useMount(mockCallback)); 12 | 13 | expect(mockCallback).toHaveBeenCalledTimes(1); 14 | }); 15 | 16 | it('should not call provided callback on unmount', () => { 17 | const { unmount } = renderHook(() => useMount(mockCallback)); 18 | expect(mockCallback).toHaveBeenCalledTimes(1); 19 | 20 | unmount(); 21 | 22 | expect(mockCallback).toHaveBeenCalledTimes(1); 23 | }); 24 | 25 | it('should not call provided callback on rerender', () => { 26 | const { rerender } = renderHook(() => useMount(mockCallback)); 27 | expect(mockCallback).toHaveBeenCalledTimes(1); 28 | 29 | rerender(); 30 | 31 | expect(mockCallback).toHaveBeenCalledTimes(1); 32 | }); 33 | -------------------------------------------------------------------------------- /docs/useSessionStorage.md: -------------------------------------------------------------------------------- 1 | # `useSessionStorage` 2 | 3 | React side-effect hook that manages a single `sessionStorage` key. 4 | 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import {useSessionStorage} from 'react-use'; 10 | 11 | const Demo = () => { 12 | const [value, setValue] = useSessionStorage('my-key', 'foo'); 13 | 14 | return ( 15 |
16 |
Value: {value}
17 | 18 | 19 |
20 | ); 21 | }; 22 | ``` 23 | 24 | 25 | ## Reference 26 | 27 | ```js 28 | useSessionStorage(key); 29 | useSessionStorage(key, initialValue); 30 | useSessionStorage(key, initialValue, raw); 31 | ``` 32 | 33 | - `key` — `sessionStorage` key to manage. 34 | - `initialValue` — initial value to set, if value in `sessionStorage` is empty. 35 | - `raw` — boolean, if set to `true`, hook will not attempt to JSON serialize stored values. 36 | -------------------------------------------------------------------------------- /docs/useAsyncFn.md: -------------------------------------------------------------------------------- 1 | # `useAsyncFn` 2 | 3 | React hook that returns state and a callback for an `async` function or a 4 | function that returns a promise. The state is of the same shape as `useAsync`. 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import {useAsyncFn} from 'react-use'; 10 | 11 | const Demo = ({url}) => { 12 | const [state, doFetch] = useAsyncFn(async () => { 13 | const response = await fetch(url); 14 | const result = await response.text(); 15 | return result 16 | }, [url]); 17 | 18 | return ( 19 |
20 | {state.loading 21 | ?
Loading...
22 | : state.error 23 | ?
Error: {state.error.message}
24 | :
Value: {state.value}
25 | } 26 | 27 |
28 | ); 29 | }; 30 | ``` 31 | 32 | ## Reference 33 | 34 | ```ts 35 | useAsyncFn(fn, deps?: any[], initialState?: AsyncState); 36 | ``` 37 | -------------------------------------------------------------------------------- /stories/useGetSet.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useState } from 'react'; 4 | import { useGetSet } from '../src'; 5 | import ShowDocs from './util/ShowDocs'; 6 | 7 | const Demo = () => { 8 | const [get, set] = useGetSet(0); 9 | const onClick = () => { 10 | setTimeout(() => { 11 | set(get() + 1); 12 | }, 1_000); 13 | }; 14 | 15 | return ; 16 | }; 17 | 18 | const DemoWrong = () => { 19 | const [cnt, set] = useState(0); 20 | const onClick = () => { 21 | setTimeout(() => { 22 | set(cnt + 1); 23 | }, 1_000); 24 | }; 25 | 26 | return ; 27 | }; 28 | 29 | storiesOf('State/useGetSet', module) 30 | .add('Docs', () => ) 31 | .add('Demo, 1s delay', () => ) 32 | .add('DemoWrong, 1s delay', () => ); 33 | -------------------------------------------------------------------------------- /tests/useMountedState.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import useMountedState from '../src/useMountedState'; 3 | 4 | describe('useMountedState', () => { 5 | it('should be defined', () => { 6 | expect(useMountedState).toBeDefined(); 7 | }); 8 | 9 | it('should return a function', () => { 10 | const hook = renderHook(() => useMountedState(), { initialProps: false }); 11 | 12 | expect(typeof hook.result.current).toEqual('function'); 13 | }); 14 | 15 | it('should return true if component is mounted', () => { 16 | const hook = renderHook(() => useMountedState(), { initialProps: false }); 17 | 18 | expect(hook.result.current()).toBeTruthy(); 19 | }); 20 | 21 | it('should return false if component is unmounted', () => { 22 | const hook = renderHook(() => useMountedState(), { initialProps: false }); 23 | 24 | hook.unmount(); 25 | 26 | expect(hook.result.current()).toBeFalsy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/useAsyncRetry.ts: -------------------------------------------------------------------------------- 1 | import { DependencyList, useCallback, useState } from 'react'; 2 | import useAsync, { AsyncState } from './useAsync'; 3 | 4 | export type AsyncStateRetry = AsyncState & { 5 | retry(): void; 6 | }; 7 | 8 | const useAsyncRetry = (fn: () => Promise, deps: DependencyList = []) => { 9 | const [attempt, setAttempt] = useState(0); 10 | const state = useAsync(fn, [...deps, attempt]); 11 | 12 | const stateLoading = state.loading; 13 | const retry = useCallback(() => { 14 | if (stateLoading) { 15 | if (process.env.NODE_ENV === 'development') { 16 | console.log( 17 | 'You are calling useAsyncRetry hook retry() method while loading in progress, this is a no-op.' 18 | ); 19 | } 20 | 21 | return; 22 | } 23 | 24 | setAttempt((currentAttempt) => currentAttempt + 1); 25 | }, [...deps, stateLoading]); 26 | 27 | return { ...state, retry }; 28 | }; 29 | 30 | export default useAsyncRetry; 31 | -------------------------------------------------------------------------------- /stories/createGlobalState.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import React, { FC } from 'react'; 3 | import { createGlobalState } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const useGlobalValue = createGlobalState(0); 7 | 8 | const CompA: FC = () => { 9 | const [value, setValue] = useGlobalValue(); 10 | 11 | return ; 12 | }; 13 | 14 | const CompB: FC = () => { 15 | const [value, setValue] = useGlobalValue(); 16 | 17 | return ; 18 | }; 19 | 20 | const Demo: FC = () => { 21 | const [value] = useGlobalValue(); 22 | return ( 23 |
24 |

{value}

25 | 26 | 27 |
28 | ); 29 | }; 30 | 31 | storiesOf('State/createGlobalState', module) 32 | .add('Docs', () => ) 33 | .add('Demo', () => ); 34 | -------------------------------------------------------------------------------- /src/useScrolling.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect, useState } from 'react'; 2 | import { off, on } from './misc/util'; 3 | 4 | const useScrolling = (ref: RefObject): boolean => { 5 | const [scrolling, setScrolling] = useState(false); 6 | 7 | useEffect(() => { 8 | if (ref.current) { 9 | let scrollingTimeout; 10 | 11 | const handleScrollEnd = () => { 12 | setScrolling(false); 13 | }; 14 | 15 | const handleScroll = () => { 16 | setScrolling(true); 17 | clearTimeout(scrollingTimeout); 18 | scrollingTimeout = setTimeout(() => handleScrollEnd(), 150); 19 | }; 20 | 21 | on(ref.current, 'scroll', handleScroll, false); 22 | return () => { 23 | if (ref.current) { 24 | off(ref.current, 'scroll', handleScroll, false); 25 | } 26 | }; 27 | } 28 | return () => {}; 29 | }, [ref]); 30 | 31 | return scrolling; 32 | }; 33 | 34 | export default useScrolling; 35 | -------------------------------------------------------------------------------- /docs/useMedia.md: -------------------------------------------------------------------------------- 1 | # `useMedia` 2 | 3 | React sensor hook that tracks state of a CSS media query. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useMedia} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const isWide = useMedia('(min-width: 480px)'); 12 | 13 | return ( 14 |
15 | Screen is wide: {isWide ? 'Yes' : 'No'} 16 |
17 | ); 18 | }; 19 | ``` 20 | 21 | ## Reference 22 | 23 | ```ts 24 | useMedia(query: string, defaultState: boolean = false): boolean; 25 | ``` 26 | 27 | The `defaultState` parameter is only used as a fallback for server side rendering. 28 | 29 | When server side rendering, it is important to set this parameter because without it the server's initial state will fallback to false, but the client will initialize to the result of the media query. When React hydrates the server render, it may not match the client's state. See the [React docs](https://reactjs.org/docs/react-dom.html#hydrate) for more on why this is can lead to costly bugs 🐛. 30 | -------------------------------------------------------------------------------- /stories/useCookie.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import React, { useState, useEffect } from 'react'; 3 | import { useCookie } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [value, updateCookie, deleteCookie] = useCookie('my-cookie'); 8 | const [counter, setCounter] = useState(1); 9 | 10 | useEffect(() => { 11 | deleteCookie(); 12 | }, []); 13 | 14 | const updateCookieHandler = () => { 15 | updateCookie(`my-awesome-cookie-${counter}`); 16 | setCounter((c) => c + 1); 17 | }; 18 | 19 | return ( 20 |
21 |

Value: {value}

22 | 23 |
24 | 25 |
26 | ); 27 | }; 28 | 29 | storiesOf('Side effects/useCookie', module) 30 | .add('Docs', () => ) 31 | .add('Demo', () => ); 32 | -------------------------------------------------------------------------------- /src/factory/createRouter.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface RouterProviderProps { 4 | route: string; 5 | fullRoute?: string; 6 | parent?: any; 7 | } 8 | 9 | const createRouter = () => { 10 | const context = React.createContext({ 11 | route: '', 12 | }); 13 | 14 | // not sure if this supposed to be unused, ignoring ts error for now 15 | // @ts-ignore 16 | const Router: React.SFC = (props) => { 17 | const { route, fullRoute, parent, children } = props; 18 | 19 | if (process.env.NODE_ENV !== 'production') { 20 | if (typeof route !== 'string') { 21 | throw new TypeError('Router route must be a string.'); 22 | } 23 | } 24 | 25 | return React.createElement(context.Provider as any, { 26 | value: { 27 | fullRoute: fullRoute || route, 28 | route, 29 | parent, 30 | }, 31 | children, 32 | }); 33 | }; 34 | }; 35 | 36 | export default createRouter; 37 | -------------------------------------------------------------------------------- /src/useClickAway.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect, useRef } from 'react'; 2 | import { off, on } from './misc/util'; 3 | 4 | const defaultEvents = ['mousedown', 'touchstart']; 5 | 6 | const useClickAway = ( 7 | ref: RefObject, 8 | onClickAway: (event: E) => void, 9 | events: string[] = defaultEvents 10 | ) => { 11 | const savedCallback = useRef(onClickAway); 12 | useEffect(() => { 13 | savedCallback.current = onClickAway; 14 | }, [onClickAway]); 15 | useEffect(() => { 16 | const handler = (event) => { 17 | const { current: el } = ref; 18 | el && !el.contains(event.target) && savedCallback.current(event); 19 | }; 20 | for (const eventName of events) { 21 | on(document, eventName, handler); 22 | } 23 | return () => { 24 | for (const eventName of events) { 25 | off(document, eventName, handler); 26 | } 27 | }; 28 | }, [events, ref]); 29 | }; 30 | 31 | export default useClickAway; 32 | -------------------------------------------------------------------------------- /docs/useCookie.md: -------------------------------------------------------------------------------- 1 | # `useCookie` 2 | 3 | React hook that returns the current value of a `cookie`, a callback to update the `cookie` 4 | and a callback to delete the `cookie.` 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import { useCookie } from "react-use"; 10 | 11 | const Demo = () => { 12 | const [value, updateCookie, deleteCookie] = useCookie("my-cookie"); 13 | const [counter, setCounter] = useState(1); 14 | 15 | useEffect(() => { 16 | deleteCookie(); 17 | }, []); 18 | 19 | const updateCookieHandler = () => { 20 | updateCookie(`my-awesome-cookie-${counter}`); 21 | setCounter(c => c + 1); 22 | }; 23 | 24 | return ( 25 |
26 |

Value: {value}

27 | 28 |
29 | 30 |
31 | ); 32 | }; 33 | ``` 34 | 35 | ## Reference 36 | 37 | ```ts 38 | const [value, updateCookie, deleteCookie] = useCookie(cookieName: string); 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/usePinchZoom.md: -------------------------------------------------------------------------------- 1 | # `usePinchZoon` 2 | 3 | React sensor hook that tracks the changes in pointer touch events and detects value of pinch difference and tell if user is zooming in or out. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import { usePinchZoon } from "react-use"; 9 | 10 | const Demo = () => { 11 | const [scale, setState] = useState(1); 12 | const scaleRef = useRef(); 13 | const { zoomingState, pinchState } = usePinchZoom(scaleRef); 14 | 15 | useEffect(() => { 16 | if (zoomingState === "ZOOM_IN") { 17 | // perform zoom in scaling 18 | setState(scale + 0.1) 19 | } else if (zoomingState === "ZOOM_OUT") { 20 | // perform zoom out in scaling 21 | setState(scale - 0.1) 22 | } 23 | }, [zoomingState]); 24 | 25 | return ( 26 |
27 | 33 |
34 | ); 35 | }; 36 | ``` 37 | -------------------------------------------------------------------------------- /stories/useLogger.story.tsx: -------------------------------------------------------------------------------- 1 | import { boolean, text, withKnobs } from '@storybook/addon-knobs'; 2 | import { storiesOf } from '@storybook/react'; 3 | import * as React from 'react'; 4 | import { useCounter, useLogger } from '../src'; 5 | import ShowDocs from './util/ShowDocs'; 6 | 7 | const Demo = (props) => { 8 | const [state, { inc }] = useCounter(0); 9 | 10 | useLogger('Demo', props, state); 11 | 12 | return ( 13 | <> 14 |

{props.title}

15 | 16 | 17 | ); 18 | }; 19 | 20 | storiesOf('Lifecycle/useLogger', module) 21 | .addDecorator(withKnobs) 22 | .add('Docs', () => ) 23 | .add('Demo', () => { 24 | const props = { 25 | title: text('title', 'Open the developer console to see logs'), 26 | bold: boolean('bold', false), 27 | }; 28 | 29 | return ; 30 | }); 31 | -------------------------------------------------------------------------------- /stories/useEvent.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useEvent, useList } from '../src'; 4 | import { CenterStory } from './util/CenterStory'; 5 | import ShowDocs from './util/ShowDocs'; 6 | 7 | const { useCallback } = React; 8 | 9 | const Demo = () => { 10 | const [list, { push, clear }] = useList(); 11 | 12 | const onKeyDown = useCallback(({ key }) => { 13 | if (key === 'r') { 14 | clear(); 15 | } 16 | push(key); 17 | }, []); 18 | 19 | useEvent('keydown', onKeyDown); 20 | 21 | return ( 22 | 23 |

24 | Press some keys on your keyboard, r key resets the 25 | list 26 |

27 |
{JSON.stringify(list, null, 4)}
28 |
29 | ); 30 | }; 31 | 32 | storiesOf('Sensors/useEvent', module) 33 | .add('Docs', () => ) 34 | .add('Demo', () => ); 35 | -------------------------------------------------------------------------------- /docs/useHash.md: -------------------------------------------------------------------------------- 1 | # `useHash` 2 | 3 | React sensor hook that tracks browser's location hash. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useHash} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const [hash, setHash] = useHash(); 12 | 13 | useMount(() => { 14 | setHash('#/path/to/page?userId=123'); 15 | }); 16 | 17 | return ( 18 |
19 |
window.location.href:
20 |
21 |
{window.location.href}
22 |
23 |
Edit hash:
24 |
25 | setHash(e.target.value)} /> 26 |
27 |
28 | ); 29 | }; 30 | ``` 31 | 32 | ## API 33 | 34 | `const [hash, setHash] = useHash()` 35 | 36 | Get latest url hash with `hash` and set url hash with `setHash`. 37 | 38 | - `hash: string`: get current url hash. listen to `hashchange` event. 39 | - `setHash: (newHash: string) => void`: change url hash. Invoke this method will trigger `hashchange` event. -------------------------------------------------------------------------------- /stories/useDeepCompareEffect.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useCounter, useDeepCompareEffect } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [countNormal, { inc: incNormal }] = useCounter(0); 8 | const [countDeep, { inc: incDeep }] = useCounter(0); 9 | const options = { max: 500 }; 10 | 11 | React.useEffect(() => { 12 | if (countNormal < options.max) { 13 | incNormal(); 14 | } 15 | }, [options]); 16 | 17 | useDeepCompareEffect(() => { 18 | if (countNormal < options.max) { 19 | incDeep(); 20 | } 21 | }, [options]); 22 | 23 | return ( 24 |
25 |

useEffect: {countNormal}

26 |

useDeepCompareEffect: {countDeep}

27 |
28 | ); 29 | }; 30 | 31 | storiesOf('Lifecycle/useDeepCompareEffect', module) 32 | .add('Docs', () => ) 33 | .add('Demo', () => ); 34 | -------------------------------------------------------------------------------- /tests/useQueue.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from '@testing-library/react-hooks'; 2 | import useQueue from '../src/useQueue'; 3 | 4 | const setUp = (initialQueue?: any[]) => renderHook(() => useQueue(initialQueue)); 5 | 6 | it('takes initial state', () => { 7 | const { result } = setUp([1, 2, 3]); 8 | const { first, last, size } = result.current; 9 | expect(first).toEqual(1); 10 | expect(last).toEqual(3); 11 | expect(size).toEqual(3); 12 | }); 13 | 14 | it('appends new member', () => { 15 | const { result } = setUp([1, 2]); 16 | act(() => { 17 | result.current.add(3); 18 | }); 19 | const { first, last, size } = result.current; 20 | expect(first).toEqual(1); 21 | expect(last).toEqual(3); 22 | expect(size).toEqual(3); 23 | }); 24 | 25 | it('pops oldest member', () => { 26 | const { result } = setUp([1, 2]); 27 | act(() => { 28 | result.current.remove(); 29 | }); 30 | const { first, size } = result.current; 31 | expect(first).toEqual(2); 32 | expect(size).toEqual(1); 33 | }); 34 | -------------------------------------------------------------------------------- /src/useHover.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { noop } from './misc/util'; 3 | 4 | const { useState } = React; 5 | 6 | export type Element = ((state: boolean) => React.ReactElement) | React.ReactElement; 7 | 8 | const useHover = (element: Element): [React.ReactElement, boolean] => { 9 | const [state, setState] = useState(false); 10 | 11 | const onMouseEnter = (originalOnMouseEnter?: any) => (event: any) => { 12 | (originalOnMouseEnter || noop)(event); 13 | setState(true); 14 | }; 15 | const onMouseLeave = (originalOnMouseLeave?: any) => (event: any) => { 16 | (originalOnMouseLeave || noop)(event); 17 | setState(false); 18 | }; 19 | 20 | if (typeof element === 'function') { 21 | element = element(state); 22 | } 23 | 24 | const el = React.cloneElement(element, { 25 | onMouseEnter: onMouseEnter(element.props.onMouseEnter), 26 | onMouseLeave: onMouseLeave(element.props.onMouseLeave), 27 | }); 28 | 29 | return [el, state]; 30 | }; 31 | 32 | export default useHover; 33 | -------------------------------------------------------------------------------- /stories/useMethods.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useMethods } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const initialState = { 7 | count: 0, 8 | }; 9 | 10 | function createMethods(state) { 11 | return { 12 | reset() { 13 | return initialState; 14 | }, 15 | increment() { 16 | return { ...state, count: state.count + 1 }; 17 | }, 18 | decrement() { 19 | return { ...state, count: state.count - 1 }; 20 | }, 21 | }; 22 | } 23 | 24 | const Demo = () => { 25 | const [state, methods] = useMethods(createMethods, initialState); 26 | 27 | return ( 28 | <> 29 |

Count: {state.count}

30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | storiesOf('State/useMethods', module) 37 | .add('Docs', () => ) 38 | .add('Demo', () => ); 39 | -------------------------------------------------------------------------------- /docs/useSpring.md: -------------------------------------------------------------------------------- 1 | # `useSpring` 2 | 3 | React animation hook that updates a single numeric value over time according 4 | to spring dynamics. 5 | 6 | ## Usage 7 | 8 | ```jsx 9 | import useSpring from 'react-use/lib/useSpring'; 10 | 11 | const Demo = () => { 12 | const [target, setTarget] = useState(50); 13 | const value = useSpring(target); 14 | 15 | return ( 16 |
17 | {value} 18 |
19 | 20 | 21 |
22 | ); 23 | }; 24 | ``` 25 | 26 | Note: Because of dependency on `rebound` you have to import this hook directly like shown above. 27 | 28 | ## Requirements 29 | 30 | Install [`rebound`](https://github.com/facebook/rebound-js) peer dependency: 31 | 32 | ```bash 33 | npm add rebound 34 | # or 35 | yarn add rebound 36 | ``` 37 | 38 | ## Reference 39 | 40 | ```js 41 | const currentValue = useSpring(targetValue); 42 | const currentValue = useSpring(targetValue, tension, friction); 43 | ``` 44 | -------------------------------------------------------------------------------- /src/misc/hookState.ts: -------------------------------------------------------------------------------- 1 | export type IHookStateInitialSetter = () => S; 2 | export type IHookStateInitAction = S | IHookStateInitialSetter; 3 | 4 | export type IHookStateSetter = ((prevState: S) => S) | (() => S); 5 | export type IHookStateSetAction = S | IHookStateSetter; 6 | 7 | export type IHookStateResolvable = S | IHookStateInitialSetter | IHookStateSetter; 8 | 9 | export function resolveHookState(nextState: IHookStateInitAction): S; 10 | export function resolveHookState( 11 | nextState: IHookStateSetAction, 12 | currentState?: C 13 | ): S; 14 | export function resolveHookState( 15 | nextState: IHookStateResolvable, 16 | currentState?: C 17 | ): S; 18 | export function resolveHookState( 19 | nextState: IHookStateResolvable, 20 | currentState?: C 21 | ): S { 22 | if (typeof nextState === 'function') { 23 | return nextState.length ? (nextState as Function)(currentState) : (nextState as Function)(); 24 | } 25 | 26 | return nextState; 27 | } 28 | -------------------------------------------------------------------------------- /src/useEnsuredForwardedRef.ts: -------------------------------------------------------------------------------- 1 | import { 2 | forwardRef, 3 | ForwardRefExoticComponent, 4 | MutableRefObject, 5 | PropsWithChildren, 6 | PropsWithoutRef, 7 | RefAttributes, 8 | RefForwardingComponent, 9 | useEffect, 10 | useRef, 11 | } from 'react'; 12 | 13 | export default function useEnsuredForwardedRef( 14 | forwardedRef: MutableRefObject 15 | ): MutableRefObject { 16 | const ensuredRef = useRef(forwardedRef && forwardedRef.current); 17 | 18 | useEffect(() => { 19 | if (!forwardedRef) { 20 | return; 21 | } 22 | forwardedRef.current = ensuredRef.current; 23 | }, [forwardedRef]); 24 | 25 | return ensuredRef; 26 | } 27 | 28 | export function ensuredForwardRef( 29 | Component: RefForwardingComponent 30 | ): ForwardRefExoticComponent & RefAttributes> { 31 | return forwardRef((props: PropsWithChildren

, ref) => { 32 | const ensuredRef = useEnsuredForwardedRef(ref as MutableRefObject); 33 | return Component(props, ensuredRef); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/useCss.ts: -------------------------------------------------------------------------------- 1 | import { create, NanoRenderer } from 'nano-css'; 2 | import { addon as addonCSSOM, CSSOMAddon } from 'nano-css/addon/cssom'; 3 | import { addon as addonVCSSOM, VCSSOMAddon } from 'nano-css/addon/vcssom'; 4 | import { cssToTree } from 'nano-css/addon/vcssom/cssToTree'; 5 | import { useMemo } from 'react'; 6 | import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect'; 7 | 8 | type Nano = NanoRenderer & CSSOMAddon & VCSSOMAddon; 9 | const nano = create() as Nano; 10 | addonCSSOM(nano); 11 | addonVCSSOM(nano); 12 | 13 | let counter = 0; 14 | 15 | const useCss = (css: object): string => { 16 | const className = useMemo(() => 'react-use-css-' + (counter++).toString(36), []); 17 | const sheet = useMemo(() => new nano.VSheet(), []); 18 | 19 | useIsomorphicLayoutEffect(() => { 20 | const tree = {}; 21 | cssToTree(tree, css, '.' + className, ''); 22 | sheet.diff(tree); 23 | 24 | return () => { 25 | sheet.diff({}); 26 | }; 27 | }); 28 | 29 | return className; 30 | }; 31 | 32 | export default useCss; 33 | -------------------------------------------------------------------------------- /src/useIntersection.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect, useState } from 'react'; 2 | 3 | const useIntersection = ( 4 | ref: RefObject, 5 | options: IntersectionObserverInit 6 | ): IntersectionObserverEntry | null => { 7 | const [intersectionObserverEntry, setIntersectionObserverEntry] = 8 | useState(null); 9 | 10 | useEffect(() => { 11 | if (ref.current && typeof IntersectionObserver === 'function') { 12 | const handler = (entries: IntersectionObserverEntry[]) => { 13 | setIntersectionObserverEntry(entries[0]); 14 | }; 15 | 16 | const observer = new IntersectionObserver(handler, options); 17 | observer.observe(ref.current); 18 | 19 | return () => { 20 | setIntersectionObserverEntry(null); 21 | observer.disconnect(); 22 | }; 23 | } 24 | return () => {}; 25 | }, [ref.current, options.threshold, options.root, options.rootMargin]); 26 | 27 | return intersectionObserverEntry; 28 | }; 29 | 30 | export default useIntersection; 31 | -------------------------------------------------------------------------------- /stories/useInterval.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useInterval, useBoolean } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [count, setCount] = React.useState(0); 8 | const [delay, setDelay] = React.useState(1000); 9 | const [isRunning, toggleIsRunning] = useBoolean(true); 10 | 11 | useInterval( 12 | () => { 13 | setCount(count + 1); 14 | }, 15 | isRunning ? delay : null 16 | ); 17 | 18 | return ( 19 |

20 |
21 | delay: setDelay(Number(event.target.value))} /> 22 |
23 |

count: {count}

24 |
25 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | storiesOf('Animation/useInterval', module) 32 | .add('Docs', () => ) 33 | .add('Demo', () => ); 34 | -------------------------------------------------------------------------------- /stories/useKeyPress.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useKeyPress } from '../src'; 4 | import { CenterStory } from './util/CenterStory'; 5 | import ShowDocs from './util/ShowDocs'; 6 | 7 | const keys = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0']; 8 | 9 | const Demo = () => { 10 | const states: boolean[] = []; 11 | for (const key of keys) { 12 | // eslint-disable-next-line react-hooks/rules-of-hooks 13 | states.push(useKeyPress(key)[0]); 14 | } 15 | 16 | return ( 17 | 18 |
19 | Try pressing numbers 20 |
21 | {states.reduce( 22 | (s, pressed, index) => s + (pressed ? (s ? ' + ' : '') + keys[index] : ''), 23 | '' 24 | )} 25 |
26 |
27 | ); 28 | }; 29 | 30 | storiesOf('Sensors/useKeyPress', module) 31 | .add('Docs', () => ) 32 | .add('Demo', () => ); 33 | -------------------------------------------------------------------------------- /src/useGetSetState.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from 'react'; 2 | import useUpdate from './useUpdate'; 3 | 4 | const useGetSetState = ( 5 | initialState: T = {} as T 6 | ): [() => T, (patch: Partial) => void] => { 7 | if (process.env.NODE_ENV !== 'production') { 8 | if (typeof initialState !== 'object') { 9 | console.error('useGetSetState initial state must be an object.'); 10 | } 11 | } 12 | 13 | const update = useUpdate(); 14 | const state = useRef({ ...(initialState as object) } as T); 15 | const get = useCallback(() => state.current, []); 16 | const set = useCallback((patch: Partial) => { 17 | if (!patch) { 18 | return; 19 | } 20 | if (process.env.NODE_ENV !== 'production') { 21 | if (typeof patch !== 'object') { 22 | console.error('useGetSetState setter patch must be an object.'); 23 | } 24 | } 25 | Object.assign(state.current, patch); 26 | update(); 27 | }, []); 28 | 29 | return [get, set]; 30 | }; 31 | 32 | export default useGetSetState; 33 | -------------------------------------------------------------------------------- /docs/useHover.md: -------------------------------------------------------------------------------- 1 | # `useHover` and `useHoverDirty` 2 | 3 | React UI sensor hooks that track if some element is being hovered 4 | by a mouse. 5 | 6 | - `useHover` accepts a React element or a function that returns one, 7 | `useHoverDirty` accepts React ref. 8 | - `useHover` sets react `onMouseEnter` and `onMouseLeave` events, 9 | `useHoverDirty` sets DOM `onmouseover` and `onmouseout` events. 10 | 11 | 12 | ## Usage 13 | 14 | ```jsx 15 | import {useHover} from 'react-use'; 16 | 17 | const Demo = () => { 18 | const element = (hovered) => 19 |
20 | Hover me! {hovered && 'Thanks!'} 21 |
; 22 | const [hoverable, hovered] = useHover(element); 23 | 24 | return ( 25 |
26 | {hoverable} 27 |
{hovered ? 'HOVERED' : ''}
28 |
29 | ); 30 | }; 31 | ``` 32 | 33 | 34 | ## Reference 35 | 36 | ```js 37 | const [newReactElement, isHovering] = useHover(reactElement); 38 | const [newReactElement, isHovering] = useHover((isHovering) => reactElement); 39 | const isHovering = useHoverDirty(ref); 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/useMouse.md: -------------------------------------------------------------------------------- 1 | # `useMouse` and `useMouseHovered` 2 | 3 | React sensor hooks that re-render on mouse position changes. `useMouse` simply tracks 4 | mouse position; `useMouseHovered` allows you to specify extra options: 5 | 6 | - `bound` — to bind mouse coordinates within the element 7 | - `whenHovered` — whether to attach `mousemove` event handler only when user hovers over the element 8 | 9 | ## Usage 10 | 11 | ```jsx 12 | import {useMouse} from 'react-use'; 13 | 14 | const Demo = () => { 15 | const ref = React.useRef(null); 16 | const {docX, docY, posX, posY, elX, elY, elW, elH} = useMouse(ref); 17 | 18 | return ( 19 |
20 |
Mouse position in document - x:{docX} y:{docY}
21 |
Mouse position in element - x:{elX} y:{elY}
22 |
Element position- x:{posX} y:{posY}
23 |
Element dimensions - {elW}x{elH}
24 |
25 | ); 26 | }; 27 | ``` 28 | 29 | ## Reference 30 | 31 | ```ts 32 | useMouse(ref); 33 | useMouseHovered(ref, {bound: false, whenHovered: false}); 34 | ``` 35 | -------------------------------------------------------------------------------- /src/factory/createStateContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext, createElement, useContext, useState } from 'react'; 2 | 3 | const createStateContext = (defaultInitialValue: T) => { 4 | const context = 5 | createContext<[T, React.Dispatch>] | undefined>(undefined); 6 | const providerFactory = (props, children) => createElement(context.Provider, props, children); 7 | 8 | const StateProvider = ({ 9 | children, 10 | initialValue, 11 | }: { 12 | children?: React.ReactNode; 13 | initialValue?: T; 14 | }) => { 15 | const state = useState(initialValue !== undefined ? initialValue : defaultInitialValue); 16 | return providerFactory({ value: state }, children); 17 | }; 18 | 19 | const useStateContext = () => { 20 | const state = useContext(context); 21 | if (state == null) { 22 | throw new Error(`useStateContext must be used inside a StateProvider.`); 23 | } 24 | return state; 25 | }; 26 | 27 | return [useStateContext, StateProvider, context] as const; 28 | }; 29 | 30 | export default createStateContext; 31 | -------------------------------------------------------------------------------- /stories/createBreakpoint.story.tsx: -------------------------------------------------------------------------------- 1 | import { withKnobs } from '@storybook/addon-knobs'; 2 | import { storiesOf } from '@storybook/react'; 3 | import React from 'react'; 4 | import { createBreakpoint } from '../src'; 5 | import ShowDocs from './util/ShowDocs'; 6 | 7 | const useBreakpointA = createBreakpoint(); 8 | const useBreakpointB = createBreakpoint({ mobileM: 350, laptop: 1024, tablet: 768 }); 9 | 10 | const Demo = () => { 11 | const breakpointA = useBreakpointA(); 12 | const breakpointB = useBreakpointB(); 13 | return ( 14 |
15 |

{'try resize your window'}

16 |

{'createBreakpoint() #default : { laptopL: 1440, laptop: 1024, tablet: 768 }'}

17 |

{breakpointA}

18 |

{'createBreakpoint({ mobileM: 350, laptop: 1024, tablet: 768 })'}

19 |

{breakpointB}

20 |
21 | ); 22 | }; 23 | 24 | storiesOf('sensors/createBreakpoint', module) 25 | .addDecorator(withKnobs) 26 | .add('Docs', () => ) 27 | .add('Demo', () => { 28 | return ; 29 | }); 30 | -------------------------------------------------------------------------------- /stories/useLocalStorage.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useLocalStorage } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [value, setValue] = useLocalStorage('hello-key', 'foo'); 8 | const [removableValue, setRemovableValue, remove] = useLocalStorage('removeable-key'); 9 | 10 | return ( 11 |
12 |
Value: {value}
13 | 14 | 15 |
16 |
17 |
Removable Value: {removableValue}
18 | 19 | 20 | 21 |
22 | ); 23 | }; 24 | 25 | storiesOf('Side effects/useLocalStorage', module) 26 | .add('Docs', () => ) 27 | .add('Demo', () => ); 28 | -------------------------------------------------------------------------------- /docs/useDrop.md: -------------------------------------------------------------------------------- 1 | # `useDrop` and `useDropArea` 2 | 3 | Triggers on file, link drop and copy-paste. 4 | 5 | `useDrop` tracks events for the whole page, `useDropArea` tracks drop events 6 | for a specific element. 7 | 8 | 9 | ## Usage 10 | 11 | `useDrop`: 12 | 13 | ```jsx 14 | import {useDrop} from 'react-use'; 15 | 16 | const Demo = () => { 17 | const state = useDrop({ 18 | onFiles: files => console.log('files', files), 19 | onUri: uri => console.log('uri', uri), 20 | onText: text => console.log('text', text), 21 | }); 22 | 23 | return ( 24 |
25 | Drop something on the page. 26 |
27 | ); 28 | }; 29 | ``` 30 | 31 | `useDropArea`: 32 | 33 | ```jsx 34 | import {useDropArea} from 'react-use'; 35 | 36 | const Demo = () => { 37 | const [bond, state] = useDropArea({ 38 | onFiles: files => console.log('files', files), 39 | onUri: uri => console.log('uri', uri), 40 | onText: text => console.log('text', text), 41 | }); 42 | 43 | return ( 44 |
45 | Drop something here. 46 |
47 | ); 48 | }; 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/useSetState.md: -------------------------------------------------------------------------------- 1 | # `useSetState` 2 | 3 | React state hook that creates `setState` method which works similar to how 4 | `this.setState` works in class components—it merges object changes into 5 | current state. 6 | 7 | 8 | ## Usage 9 | 10 | ```jsx 11 | import {useSetState} from 'react-use'; 12 | 13 | const Demo = () => { 14 | const [state, setState] = useSetState({}); 15 | 16 | return ( 17 |
18 |
{JSON.stringify(state, null, 2)}
19 | 20 | 21 | 30 |
31 | ); 32 | }; 33 | ``` 34 | 35 | ## Reference 36 | 37 | ```js 38 | const [state, setState] = useSetState({cnt: 0}); 39 | 40 | setState({cnt: state.cnt + 1}); 41 | setState((prevState) => ({ 42 | cnt: prevState + 1, 43 | })); 44 | ``` 45 | -------------------------------------------------------------------------------- /src/useRaf.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect'; 3 | 4 | const useRaf = (ms: number = 1e12, delay: number = 0): number => { 5 | const [elapsed, set] = useState(0); 6 | 7 | useIsomorphicLayoutEffect(() => { 8 | let raf; 9 | let timerStop; 10 | let start; 11 | 12 | const onFrame = () => { 13 | const time = Math.min(1, (Date.now() - start) / ms); 14 | set(time); 15 | loop(); 16 | }; 17 | const loop = () => { 18 | raf = requestAnimationFrame(onFrame); 19 | }; 20 | const onStart = () => { 21 | timerStop = setTimeout(() => { 22 | cancelAnimationFrame(raf); 23 | set(1); 24 | }, ms); 25 | start = Date.now(); 26 | loop(); 27 | }; 28 | const timerDelay = setTimeout(onStart, delay); 29 | 30 | return () => { 31 | clearTimeout(timerStop); 32 | clearTimeout(timerDelay); 33 | cancelAnimationFrame(raf); 34 | }; 35 | }, [ms, delay]); 36 | 37 | return elapsed; 38 | }; 39 | 40 | export default useRaf; 41 | -------------------------------------------------------------------------------- /src/useVibrate.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { isNavigator, noop } from './misc/util'; 3 | 4 | export type VibrationPattern = number | number[]; 5 | 6 | const isVibrationApiSupported = isNavigator && 'vibrate' in navigator; 7 | 8 | function useVibrate( 9 | enabled: boolean = true, 10 | pattern: VibrationPattern = [1000, 1000], 11 | loop: boolean = true 12 | ): void { 13 | useEffect(() => { 14 | let interval; 15 | 16 | if (enabled) { 17 | navigator.vibrate(pattern); 18 | 19 | if (loop) { 20 | const duration = 21 | pattern instanceof Array ? pattern.reduce((a, b) => a + b) : (pattern as number); 22 | 23 | interval = setInterval(() => { 24 | navigator.vibrate(pattern); 25 | }, duration); 26 | } 27 | } 28 | 29 | return () => { 30 | if (enabled) { 31 | navigator.vibrate(0); 32 | 33 | if (loop) { 34 | clearInterval(interval); 35 | } 36 | } 37 | }; 38 | }, [enabled]); 39 | } 40 | 41 | export default isVibrationApiSupported ? useVibrate : noop; 42 | -------------------------------------------------------------------------------- /stories/useRafLoop.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useRafLoop, useUpdate } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [ticks, setTicks] = React.useState(0); 8 | const [lastCall, setLastCall] = React.useState(0); 9 | const update = useUpdate(); 10 | 11 | const [loopStop, loopStart, isActive] = useRafLoop((time) => { 12 | setTicks((ticks) => ticks + 1); 13 | setLastCall(time); 14 | }); 15 | 16 | return ( 17 |
18 |
RAF triggered: {ticks} (times)
19 |
Last high res timestamp: {lastCall}
20 |
21 | 28 |
29 | ); 30 | }; 31 | 32 | storiesOf('Side effects/useRafLoop', module) 33 | .add('Docs', () => ) 34 | .add('Demo', () => ); 35 | -------------------------------------------------------------------------------- /tests/useTitle.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import useTitle from '../src/useTitle'; 3 | 4 | describe('useTitle', () => { 5 | it('should be defined', () => { 6 | expect(useTitle).toBeDefined(); 7 | }); 8 | 9 | it('should update document title', () => { 10 | const hook = renderHook((props) => useTitle(props), { initialProps: 'My page title' }); 11 | 12 | expect(document.title).toBe('My page title'); 13 | hook.rerender('My other page title'); 14 | expect(document.title).toBe('My other page title'); 15 | }); 16 | 17 | it('should restore document title on unmount', () => { 18 | renderHook((props) => useTitle(props), { initialProps: 'Old Title' }); 19 | expect(document.title).toBe('Old Title'); 20 | 21 | const hook = renderHook((props) => useTitle(props.title, { restoreOnUnmount: props.restore }), { 22 | initialProps: { title: 'New Title', restore: true }, 23 | }); 24 | expect(document.title).toBe('New Title'); 25 | hook.unmount(); 26 | expect(document.title).toBe('Old Title'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/useSearchParam.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { isBrowser, off, on } from './misc/util'; 3 | 4 | const getValue = (search: string, param: string) => new URLSearchParams(search).get(param); 5 | 6 | export type UseQueryParam = (param: string) => string | null; 7 | 8 | const useSearchParam: UseQueryParam = (param) => { 9 | const location = window.location; 10 | const [value, setValue] = useState(() => getValue(location.search, param)); 11 | 12 | useEffect(() => { 13 | const onChange = () => { 14 | setValue(getValue(location.search, param)); 15 | }; 16 | 17 | on(window, 'popstate', onChange); 18 | on(window, 'pushstate', onChange); 19 | on(window, 'replacestate', onChange); 20 | 21 | return () => { 22 | off(window, 'popstate', onChange); 23 | off(window, 'pushstate', onChange); 24 | off(window, 'replacestate', onChange); 25 | }; 26 | }, []); 27 | 28 | return value; 29 | }; 30 | 31 | const useSearchParamServer = () => null; 32 | 33 | export default isBrowser ? useSearchParam : useSearchParamServer; 34 | -------------------------------------------------------------------------------- /src/useThrottleFn.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import useUnmount from './useUnmount'; 3 | 4 | const useThrottleFn = (fn: (...args: U) => T, ms: number = 200, args: U) => { 5 | const [state, setState] = useState(null); 6 | const timeout = useRef>(); 7 | const nextArgs = useRef(); 8 | 9 | useEffect(() => { 10 | if (!timeout.current) { 11 | setState(fn(...args)); 12 | const timeoutCallback = () => { 13 | if (nextArgs.current) { 14 | setState(fn(...nextArgs.current)); 15 | nextArgs.current = undefined; 16 | timeout.current = setTimeout(timeoutCallback, ms); 17 | } else { 18 | timeout.current = undefined; 19 | } 20 | }; 21 | timeout.current = setTimeout(timeoutCallback, ms); 22 | } else { 23 | nextArgs.current = args; 24 | } 25 | }, args); 26 | 27 | useUnmount(() => { 28 | timeout.current && clearTimeout(timeout.current); 29 | }); 30 | 31 | return state; 32 | }; 33 | 34 | export default useThrottleFn; 35 | -------------------------------------------------------------------------------- /docs/useGetSet.md: -------------------------------------------------------------------------------- 1 | # `useGetSet` 2 | 3 | React state hook that returns state getter function instead of 4 | raw state itself, this prevents subtle bugs when state is used 5 | in nested functions. 6 | 7 | 8 | ## Usage 9 | 10 | Below example uses `useGetSet` to increment a number after 1 second 11 | on each click. 12 | 13 | ```jsx 14 | import {useGetSet} from 'react-use'; 15 | 16 | const Demo = () => { 17 | const [get, set] = useGetSet(0); 18 | const onClick = () => { 19 | setTimeout(() => { 20 | set(get() + 1) 21 | }, 1_000); 22 | }; 23 | 24 | return ( 25 | 26 | ); 27 | }; 28 | ``` 29 | 30 | If you would do this example in a naive way using regular `useState` 31 | hook, the counter would not increment correctly if you click fast multiple times. 32 | 33 | ```jsx 34 | const DemoWrong = () => { 35 | const [cnt, set] = useState(0); 36 | const onClick = () => { 37 | setTimeout(() => { 38 | set(cnt + 1) 39 | }, 1_000); 40 | }; 41 | 42 | return ( 43 | 44 | ); 45 | }; 46 | ``` 47 | -------------------------------------------------------------------------------- /stories/useMouse.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useMouse } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const ref = React.useRef(null); 8 | const state = useMouse(ref); 9 | 10 | return ( 11 | <> 12 |
{JSON.stringify(state, null, 2)}
13 |
14 |
15 |
23 | 31 | 🐭 32 | 33 |
34 | 35 | ); 36 | }; 37 | 38 | storiesOf('Sensors/useMouse', module) 39 | .add('Docs', () => ) 40 | .add('Demo', () => ); 41 | -------------------------------------------------------------------------------- /stories/useStartTyping.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useStartTyping } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const input = React.useRef(null); 8 | useStartTyping(() => { 9 | if (input.current) { 10 | input.current.focus(); 11 | } 12 | }); 13 | 14 | return ( 15 |
16 |

Start typing, and below field will get focused.

17 | 18 | 19 |
20 |
21 | 22 |

Try focusing below elements and see what happens.

23 | 24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 |
32 |
Editable DIV
33 |
34 | ); 35 | }; 36 | 37 | storiesOf('Sensors/useStartTyping', module) 38 | .add('Docs', () => ) 39 | .add('Demo', () => ); 40 | -------------------------------------------------------------------------------- /src/factory/createBreakpoint.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from 'react'; 2 | import { isBrowser, off, on } from '../misc/util'; 3 | 4 | const createBreakpoint = 5 | (breakpoints: { [name: string]: number } = { laptopL: 1440, laptop: 1024, tablet: 768 }) => 6 | () => { 7 | const [screen, setScreen] = useState(isBrowser ? window.innerWidth : 0); 8 | 9 | useEffect(() => { 10 | const setSideScreen = (): void => { 11 | setScreen(window.innerWidth); 12 | }; 13 | setSideScreen(); 14 | on(window, 'resize', setSideScreen); 15 | return () => { 16 | off(window, 'resize', setSideScreen); 17 | }; 18 | }); 19 | const sortedBreakpoints = useMemo( 20 | () => Object.entries(breakpoints).sort((a, b) => (a[1] >= b[1] ? 1 : -1)), 21 | [breakpoints] 22 | ); 23 | const result = sortedBreakpoints.reduce((acc, [name, width]) => { 24 | if (screen >= width) { 25 | return name; 26 | } else { 27 | return acc; 28 | } 29 | }, sortedBreakpoints[0][0]); 30 | return result; 31 | }; 32 | 33 | export default createBreakpoint; 34 | -------------------------------------------------------------------------------- /src/useUnmountPromise.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useRef } from 'react'; 2 | import useEffectOnce from './useEffectOnce'; 3 | 4 | export type Race =

, E = any>(promise: P, onError?: (error: E) => void) => P; 5 | 6 | const useUnmountPromise = (): Race => { 7 | const refUnmounted = useRef(false); 8 | useEffectOnce(() => () => { 9 | refUnmounted.current = true; 10 | }); 11 | 12 | const wrapper = useMemo(() => { 13 | const race =

, E>(promise: P, onError?: (error: E) => void) => { 14 | const newPromise: P = new Promise((resolve, reject) => { 15 | promise.then( 16 | (result) => { 17 | if (!refUnmounted.current) resolve(result); 18 | }, 19 | (error) => { 20 | if (!refUnmounted.current) reject(error); 21 | else if (onError) onError(error); 22 | else console.error('useUnmountPromise', error); 23 | } 24 | ); 25 | }) as P; 26 | return newPromise; 27 | }; 28 | return race; 29 | }, []); 30 | 31 | return wrapper; 32 | }; 33 | 34 | export default useUnmountPromise; 35 | -------------------------------------------------------------------------------- /docs/useInterval.md: -------------------------------------------------------------------------------- 1 | # `useInterval` 2 | 3 | A declarative interval hook based on [Dan Abramov's article on overreacted.io](https://overreacted.io/making-setinterval-declarative-with-react-hooks). The interval can be paused by setting the delay to `null`. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import * as React from 'react'; 9 | import {useInterval} from 'react-use'; 10 | 11 | const Demo = () => { 12 | const [count, setCount] = React.useState(0); 13 | const [delay, setDelay] = React.useState(1000); 14 | const [isRunning, toggleIsRunning] = useBoolean(true); 15 | 16 | useInterval( 17 | () => { 18 | setCount(count + 1); 19 | }, 20 | isRunning ? delay : null 21 | ); 22 | 23 | return ( 24 |

25 |
26 | delay: setDelay(Number(event.target.value))} /> 27 |
28 |

count: {count}

29 |
30 | 31 |
32 |
33 | ); 34 | }; 35 | ``` 36 | 37 | ## Reference 38 | 39 | ```js 40 | useInterval(callback, delay?: number) 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/useKeyboardJs.md: -------------------------------------------------------------------------------- 1 | # `useKeyboardJs` 2 | 3 | React UI sensor hook that detects complex key combos like detecting when 4 | multiple keys are held down at the same time or requiring them to be held down in a specified order. 5 | 6 | Via [KeyboardJS key combos](https://github.com/RobertWHurst/KeyboardJS). 7 | Check its documentation for further details on how to make combo strings. 8 | 9 | ## Usage 10 | 11 | ```jsx 12 | import useKeyboardJs from 'react-use/lib/useKeyboardJs'; 13 | 14 | const Demo = () => { 15 | const [isPressed] = useKeyboardJs('a + b'); 16 | 17 | return ( 18 |
19 | [a + b] pressed: {isPressed ? 'Yes' : 'No'} 20 |
21 | ); 22 | }; 23 | ``` 24 | 25 | Note: Because of dependency on `keyboardjs` you have to import this hook directly like shown above. 26 | 27 | ## Requirements 28 | 29 | Install [`keyboardjs`](https://github.com/RobertWHurst/KeyboardJS) peer dependency: 30 | 31 | ```bash 32 | npm add keyboardjs 33 | # or 34 | yarn add keyboardjs 35 | ``` 36 | 37 | ## Reference 38 | 39 | ```js 40 | useKeyboardJs(combination: string | string[]): [isPressed: boolean, event?: KeyboardEvent] 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/useLatest.md: -------------------------------------------------------------------------------- 1 | # `useLatest` 2 | 3 | React state hook that returns the latest state as described in the [React hooks FAQ](https://reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function). 4 | 5 | This is mostly useful to get access to the latest value of some props or state inside an asynchronous callback, instead of that value at the time the callback was created from. 6 | 7 | ## Usage 8 | 9 | ```jsx 10 | import { useLatest } from 'react-use'; 11 | 12 | const Demo = () => { 13 | const [count, setCount] = React.useState(0); 14 | const latestCount = useLatest(count); 15 | 16 | function handleAlertClick() { 17 | setTimeout(() => { 18 | alert(`Latest count value: ${latestCount.current}`); 19 | }, 3000); 20 | } 21 | 22 | return ( 23 |
24 |

You clicked {count} times

25 | 26 | 27 |
28 | ); 29 | }; 30 | ``` 31 | 32 | ## Reference 33 | 34 | ```ts 35 | const latestState = useLatest = (state: T): MutableRefObject; 36 | ``` 37 | -------------------------------------------------------------------------------- /stories/useRafState.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useRafState, useMount } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [state, setState] = useRafState({ x: 0, y: 0 }); 8 | 9 | useMount(() => { 10 | const onMouseMove = (event: MouseEvent) => { 11 | setState({ x: event.clientX, y: event.clientY }); 12 | }; 13 | const onTouchMove = (event: TouchEvent) => { 14 | setState({ x: event.changedTouches[0].clientX, y: event.changedTouches[0].clientY }); 15 | }; 16 | 17 | document.addEventListener('mousemove', onMouseMove); 18 | document.addEventListener('touchmove', onTouchMove); 19 | 20 | return () => { 21 | document.removeEventListener('mousemove', onMouseMove); 22 | document.removeEventListener('touchmove', onTouchMove); 23 | }; 24 | }); 25 | 26 | return
{JSON.stringify(state, null, 2)}
; 27 | }; 28 | 29 | storiesOf('State/useRafState', module) 30 | .add('Docs', () => ) 31 | .add('Demo', () => ); 32 | -------------------------------------------------------------------------------- /src/useThrottle.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import useUnmount from './useUnmount'; 3 | 4 | const useThrottle = (value: T, ms: number = 200) => { 5 | const [state, setState] = useState(value); 6 | const timeout = useRef>(); 7 | const nextValue = useRef(null) as any; 8 | const hasNextValue = useRef(0) as any; 9 | 10 | useEffect(() => { 11 | if (!timeout.current) { 12 | setState(value); 13 | const timeoutCallback = () => { 14 | if (hasNextValue.current) { 15 | hasNextValue.current = false; 16 | setState(nextValue.current); 17 | timeout.current = setTimeout(timeoutCallback, ms); 18 | } else { 19 | timeout.current = undefined; 20 | } 21 | }; 22 | timeout.current = setTimeout(timeoutCallback, ms); 23 | } else { 24 | nextValue.current = value; 25 | hasNextValue.current = true; 26 | } 27 | }, [value]); 28 | 29 | useUnmount(() => { 30 | timeout.current && clearTimeout(timeout.current); 31 | }); 32 | 33 | return state; 34 | }; 35 | 36 | export default useThrottle; 37 | -------------------------------------------------------------------------------- /stories/useAsyncFn.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useAsyncFn } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [state, callback] = useAsyncFn( 8 | () => 9 | new Promise((resolve, reject) => { 10 | setTimeout(() => { 11 | if (Math.random() > 0.5) { 12 | resolve('✌️'); 13 | } else { 14 | reject(new Error('A pseudo random error occurred')); 15 | } 16 | }, 1000); 17 | }) 18 | ); 19 | 20 | return ( 21 |
22 | {state.loading ? ( 23 |

Loading...

24 | ) : state.error ? ( 25 |

Error: {state.error.message}

26 | ) : ( 27 |

Value: {state.value}

28 | )} 29 | 30 |
{JSON.stringify(state, null, 2)}
31 |
32 | ); 33 | }; 34 | 35 | storiesOf('Side effects/useAsyncFn', module) 36 | .add('Docs', () => ) 37 | .add('Demo', () => ); 38 | -------------------------------------------------------------------------------- /stories/useStateValidator.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import useStateValidator from '../src/useStateValidator'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const DemoStateValidator = (s) => 7 | [s === '' ? undefined : (s * 1) % 2 === 0] as [boolean | undefined]; 8 | const Demo = () => { 9 | const [state, setState] = React.useState(0); 10 | const [[isValid]] = useStateValidator(state, DemoStateValidator); 11 | 12 | return ( 13 |
14 |
Below field is valid only if number is even
15 | ) => { 21 | setState(ev.target.value as unknown as number); 22 | }} 23 | /> 24 | {isValid !== undefined && {isValid ? 'Valid!' : 'Invalid'}} 25 |
26 | ); 27 | }; 28 | 29 | storiesOf('State/useStateValidator', module) 30 | .add('Docs', () => ) 31 | .add('Demo', () => ); 32 | -------------------------------------------------------------------------------- /src/useShallowCompareEffect.ts: -------------------------------------------------------------------------------- 1 | import { DependencyList, EffectCallback } from 'react'; 2 | import { equal as isShallowEqual } from 'fast-shallow-equal'; 3 | import useCustomCompareEffect from './useCustomCompareEffect'; 4 | 5 | const isPrimitive = (val: any) => val !== Object(val); 6 | const shallowEqualDepsList = (prevDeps: DependencyList, nextDeps: DependencyList) => 7 | prevDeps.every((dep, index) => isShallowEqual(dep, nextDeps[index])); 8 | 9 | const useShallowCompareEffect = (effect: EffectCallback, deps: DependencyList) => { 10 | if (process.env.NODE_ENV !== 'production') { 11 | if (!(deps instanceof Array) || !deps.length) { 12 | console.warn( 13 | '`useShallowCompareEffect` should not be used with no dependencies. Use React.useEffect instead.' 14 | ); 15 | } 16 | 17 | if (deps.every(isPrimitive)) { 18 | console.warn( 19 | '`useShallowCompareEffect` should not be used with dependencies that are all primitive values. Use React.useEffect instead.' 20 | ); 21 | } 22 | } 23 | 24 | useCustomCompareEffect(effect, deps, shallowEqualDepsList); 25 | }; 26 | 27 | export default useShallowCompareEffect; 28 | -------------------------------------------------------------------------------- /docs/useCopyToClipboard.md: -------------------------------------------------------------------------------- 1 | # `useCopyToClipboard` 2 | 3 | Copy text to a user's clipboard. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | const Demo = () => { 9 | const [text, setText] = React.useState(''); 10 | const [state, copyToClipboard] = useCopyToClipboard(); 11 | 12 | return ( 13 |
14 | setText(e.target.value)} /> 15 | 16 | {state.error 17 | ?

Unable to copy value: {state.error.message}

18 | : state.value &&

Copied {state.value}

} 19 |
20 | ) 21 | } 22 | ``` 23 | 24 | ## Reference 25 | 26 | ```js 27 | const [{value, error, noUserInteraction}, copyToClipboard] = useCopyToClipboard(); 28 | ``` 29 | 30 | - `value` — value that was copied to clipboard, undefined when nothing was copied. 31 | - `error` — caught error when trying to copy to clipboard. 32 | - `noUserInteraction` — boolean indicating if user interaction was required to copy the value to clipboard to expose full API from underlying [`copy-to-clipboard`](https://github.com/sudodoki/copy-to-clipboard) library. 33 | -------------------------------------------------------------------------------- /docs/useIntersection.md: -------------------------------------------------------------------------------- 1 | # `useIntersection` 2 | 3 | React sensor hook that tracks the changes in the intersection of a target element with an ancestor element or with a top-level document's viewport. Uses the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) and returns a [IntersectionObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry). 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import * as React from 'react'; 9 | import { useIntersection } from 'react-use'; 10 | 11 | const Demo = () => { 12 | const intersectionRef = React.useRef(null); 13 | const intersection = useIntersection(intersectionRef, { 14 | root: null, 15 | rootMargin: '0px', 16 | threshold: 1 17 | }); 18 | 19 | return ( 20 |
21 | {intersection && intersection.intersectionRatio < 1 22 | ? 'Obscured' 23 | : 'Fully in view'} 24 |
25 | ); 26 | }; 27 | ``` 28 | 29 | ## Reference 30 | 31 | ```ts 32 | useIntersection( 33 | ref: RefObject, 34 | options: IntersectionObserverInit, 35 | ): IntersectionObserverEntry | null; 36 | ``` 37 | -------------------------------------------------------------------------------- /stories/useKeyboardJs.story.tsx: -------------------------------------------------------------------------------- 1 | import { text, withKnobs } from '@storybook/addon-knobs'; 2 | import { storiesOf } from '@storybook/react'; 3 | import * as React from 'react'; 4 | import useKeyboardJs from '../src/useKeyboardJs'; 5 | import { CenterStory } from './util/CenterStory'; 6 | import ShowDocs from './util/ShowDocs'; 7 | 8 | const Demo = ({ combo }) => { 9 | const [pressed] = useKeyboardJs(combo); 10 | 11 | return ( 12 | 13 |
14 | Press{' '} 15 | 17 | {combo} 18 | {' '} 19 | combo 20 |
21 |
22 |
{pressed ? '💋' : ''}
23 |
24 |
25 | ); 26 | }; 27 | 28 | storiesOf('Sensors/useKeyboardJs', module) 29 | .addDecorator(withKnobs) 30 | .add('Docs', () => ) 31 | .add('Demo', () => { 32 | const combo = text('Combo', 'i + l + u'); 33 | return ; 34 | }); 35 | -------------------------------------------------------------------------------- /docs/useSearchParam.md: -------------------------------------------------------------------------------- 1 | # `useSearchParam` 2 | 3 | React sensor hook that tracks browser's location search param. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import {useSearchParam} from 'react-use'; 9 | 10 | const Demo = () => { 11 | const edit = useSearchParam('edit'); 12 | 13 | return ( 14 |
15 |
edit: {edit || '🤷‍♂️'}
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | ); 27 | }; 28 | ``` 29 | 30 | ## Caveats/Gotchas 31 | 32 | When using a hash router, like `react-router`'s [``](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/api/HashRouter.md), this hook won't be able to read the search parameters as they are considered part of the hash of the URL by browsers. 33 | -------------------------------------------------------------------------------- /stories/useSearchParam.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useSearchParam } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const edit = useSearchParam('edit'); 8 | 9 | return ( 10 |
11 |
edit: {edit || '🤷‍♂️'}
12 |
13 | 17 |
18 |
19 | 23 |
24 |
25 | 28 |
29 |
30 | ); 31 | }; 32 | 33 | storiesOf('Sensors/useSearchParam', module) 34 | .add('Docs', () => ) 35 | .add('Demo', () => ); 36 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { compilerOptions } = require('../tsconfig.json'); 3 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 4 | 5 | const basedir = path.join(__dirname, '..'); 6 | 7 | module.exports = async ({ config, mode }) => { 8 | config.module.rules.push( 9 | { 10 | test: /\.md?$/, 11 | loader: "markdown-loader", 12 | }, 13 | { 14 | test: /\.tsx?$/, 15 | loader: 'ts-loader', 16 | include: [ 17 | path.join(basedir, 'src'), 18 | path.join(basedir, 'stories'), 19 | ], 20 | options: { 21 | transpileOnly: true, // use transpileOnly mode to speed-up compilation 22 | compilerOptions: { 23 | ...compilerOptions, 24 | declaration: false, 25 | }, 26 | }, 27 | }, 28 | ); 29 | 30 | config.plugins.push(new ForkTsCheckerWebpackPlugin()); 31 | 32 | config.resolve.extensions = ['.ts', '.tsx', '.js', '.jsx']; 33 | config.resolve.enforceExtension = false; 34 | 35 | // disable the hint about too big bundle 36 | config.performance.hints = false; 37 | 38 | return config; 39 | }; 40 | -------------------------------------------------------------------------------- /docs/useMethods.md: -------------------------------------------------------------------------------- 1 | # `useMethods` 2 | 3 | React hook that simplifies the `useReducer` implementation. 4 | 5 | ## Usage 6 | 7 | ```jsx 8 | import { useMethods } from 'react-use'; 9 | 10 | const initialState = { 11 | count: 0, 12 | }; 13 | 14 | function createMethods(state) { 15 | return { 16 | reset() { 17 | return initialState; 18 | }, 19 | increment() { 20 | return { ...state, count: state.count + 1 }; 21 | }, 22 | decrement() { 23 | return { ...state, count: state.count - 1 }; 24 | }, 25 | }; 26 | } 27 | 28 | const Demo = () => { 29 | const [state, methods] = useMethods(createMethods, initialState); 30 | 31 | return ( 32 | <> 33 |

Count: {state.count}

34 | 35 | 36 | 37 | ); 38 | }; 39 | ``` 40 | 41 | ## Reference 42 | 43 | ```js 44 | const [state, methods] = useMethods(createMethods, initialState); 45 | ``` 46 | 47 | - `createMethods` — function that takes current state and return an object containing methods that return updated state. 48 | - `initialState` — initial value of the state. 49 | -------------------------------------------------------------------------------- /docs/useUpsert.md: -------------------------------------------------------------------------------- 1 | # `useUpsert` 2 | 3 | > DEPRECATED! 4 | > Use `useList` hook's upsert action instead 5 | 6 | Superset of [`useList`](./useList.md). Provides an additional method to upsert (update or insert) an element into the list. 7 | 8 | ## Usage 9 | 10 | ```jsx 11 | import {useUpsert} from 'react-use'; 12 | 13 | const Demo = () => { 14 | const comparisonFunction = (a: DemoType, b: DemoType) => { 15 | return a.id === b.id; 16 | }; 17 | const [list, { set, upsert, remove }] = useUpsert(comparisonFunction, initialItems); 18 | 19 | return ( 20 |
21 | {list.map((item: DemoType, index: number) => ( 22 |
23 | upsert({ ...item, text: e.target.value })} /> 24 | 25 |
26 | ))} 27 | 28 | 29 |
30 | ); 31 | }; 32 | ``` 33 | 34 | ## Related hooks 35 | 36 | - [useList](./useList.md) 37 | -------------------------------------------------------------------------------- /stories/useCustomCompareEffect.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useCounter, useCustomCompareEffect } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | import isDeepEqual from '../src/misc/isDeepEqual'; 6 | 7 | const Demo = () => { 8 | const [countNormal, { inc: incNormal }] = useCounter(0); 9 | const [countDeep, { inc: incDeep }] = useCounter(0); 10 | const options = { max: 500 }; 11 | 12 | React.useEffect(() => { 13 | if (countNormal < options.max) { 14 | incNormal(); 15 | } 16 | }, [options]); 17 | 18 | useCustomCompareEffect( 19 | () => { 20 | if (countNormal < options.max) { 21 | incDeep(); 22 | } 23 | }, 24 | [options], 25 | (prevDeps, nextDeps) => isDeepEqual(prevDeps, nextDeps) 26 | ); 27 | 28 | return ( 29 |
30 |

useEffect: {countNormal}

31 |

useCustomCompareEffect: {countDeep}

32 |
33 | ); 34 | }; 35 | 36 | storiesOf('Lifecycle/useCustomCompareEffect', module) 37 | .add('Docs', () => ) 38 | .add('Demo', () => ); 39 | -------------------------------------------------------------------------------- /tests/misc/hookState.test.ts: -------------------------------------------------------------------------------- 1 | import { resolveHookState } from '../../src/misc/hookState'; 2 | 3 | describe('resolveHookState', () => { 4 | it('should defined', () => { 5 | expect(resolveHookState).toBeDefined(); 6 | }); 7 | 8 | it(`should return value as is if it's not a function`, () => { 9 | expect(resolveHookState(1)).toBe(1); 10 | expect(resolveHookState('HI!')).toBe('HI!'); 11 | expect(resolveHookState(undefined)).toBe(undefined); 12 | }); 13 | 14 | it('should call passed function', () => { 15 | const spy = jest.fn(); 16 | resolveHookState(spy); 17 | expect(spy).toHaveBeenCalled(); 18 | }); 19 | 20 | it('should pass 2nd parameter to function if it awaited', () => { 21 | const spy = jest.fn((n: number) => n); 22 | resolveHookState(spy, 123); 23 | expect(spy).toHaveBeenCalled(); 24 | expect(spy.mock.calls[0][0]).toBe(123); 25 | }); 26 | 27 | it('should not pass 2nd parameter to function if it not awaited', () => { 28 | const spy = jest.fn(() => {}); 29 | /* @ts-expect-error */ 30 | resolveHookState(spy, 123); 31 | expect(spy).toHaveBeenCalled(); 32 | expect(spy.mock.calls[0].length).toBe(0); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/useMediaDevices.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { isNavigator, noop, off, on } from './misc/util'; 3 | 4 | const useMediaDevices = () => { 5 | const [state, setState] = useState({}); 6 | 7 | useEffect(() => { 8 | let mounted = true; 9 | 10 | const onChange = () => { 11 | navigator.mediaDevices 12 | .enumerateDevices() 13 | .then((devices) => { 14 | if (mounted) { 15 | setState({ 16 | devices: devices.map(({ deviceId, groupId, kind, label }) => ({ 17 | deviceId, 18 | groupId, 19 | kind, 20 | label, 21 | })), 22 | }); 23 | } 24 | }) 25 | .catch(noop); 26 | }; 27 | 28 | on(navigator.mediaDevices, 'devicechange', onChange); 29 | onChange(); 30 | 31 | return () => { 32 | mounted = false; 33 | off(navigator.mediaDevices, 'devicechange', onChange); 34 | }; 35 | }, []); 36 | 37 | return state; 38 | }; 39 | 40 | const useMediaDevicesMock = () => ({}); 41 | 42 | export default isNavigator && !!navigator.mediaDevices ? useMediaDevices : useMediaDevicesMock; 43 | -------------------------------------------------------------------------------- /src/useTimeoutFn.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | 3 | export type UseTimeoutFnReturn = [() => boolean | null, () => void, () => void]; 4 | 5 | export default function useTimeoutFn(fn: Function, ms: number = 0): UseTimeoutFnReturn { 6 | const ready = useRef(false); 7 | const timeout = useRef>(); 8 | const callback = useRef(fn); 9 | 10 | const isReady = useCallback(() => ready.current, []); 11 | 12 | const set = useCallback(() => { 13 | ready.current = false; 14 | timeout.current && clearTimeout(timeout.current); 15 | 16 | timeout.current = setTimeout(() => { 17 | ready.current = true; 18 | callback.current(); 19 | }, ms); 20 | }, [ms]); 21 | 22 | const clear = useCallback(() => { 23 | ready.current = null; 24 | timeout.current && clearTimeout(timeout.current); 25 | }, []); 26 | 27 | // update ref when function changes 28 | useEffect(() => { 29 | callback.current = fn; 30 | }, [fn]); 31 | 32 | // set on mount, clear on unmount 33 | useEffect(() => { 34 | set(); 35 | 36 | return clear; 37 | }, [ms]); 38 | 39 | return [isReady, clear, set]; 40 | } 41 | -------------------------------------------------------------------------------- /stories/useCopyToClipboard.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import * as React from 'react'; 3 | import { useCopyToClipboard } from '../src'; 4 | import ShowDocs from './util/ShowDocs'; 5 | 6 | const Demo = () => { 7 | const [text, setText] = React.useState(''); 8 | const [state, copyToClipboard] = useCopyToClipboard(); 9 | 10 | return ( 11 |
12 | setText(e.target.value)} /> 13 | 16 | {state.error ? ( 17 |

Unable to copy value: {state.error.message}

18 | ) : ( 19 | state.value && ( 20 | <> 21 |

22 | Copied {state.value} {state.noUserInteraction ? 'without' : 'with'} user interaction 23 |

24 | 25 | 26 | ) 27 | )} 28 |
29 | ); 30 | }; 31 | 32 | storiesOf('Side-effects/useCopyToClipboard', module) 33 | .add('Docs', () => ) 34 | .add('Demo', () => ); 35 | --------------------------------------------------------------------------------