React钩子封装记录 副作用钩子 用于设置导航栏标题。
语法:1 useTitle(title, options)
参数:
title:{String},标题文案
options:{Object},参数,可选
restoreOnUnmount:{Boolean}是否退出还原标题
使用:1 2 3 4 5 6 7 import {useTitle} from 'react-use' ;const Demo = () => { useTitle('Hello title!' ); return null ; };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { useRef, useEffect } from 'react' ;export interface UseTitleOptions { restoreOnUnmount?: boolean; } const DEFAULT_USE_TITLE_OPTIONS: UseTitleOptions = { restoreOnUnmount: false , }; function useTitle (title: string, options: UseTitleOptions = DEFAULT_USE_TITLE_OPTIONS ) { const prevTitleRef = useRef(document .title); document .title = title; useEffect(() => { if (options && options.restoreOnUnmount) { return () => { document .title = prevTitleRef.current; }; } else { return ; } }, []); } export default typeof document !== 'undefined' ? useTitle : (_title: string ) => {};
其中主要是利用document.title
来设置页面标题,还原标题则是借助ref来进行旧标题暂存。
用于cookie的处理。
语法:1 const [value, updateCookie, deleteCookie] = useTitle(cookieName)
参数:
cookieName{String},cookie标识
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { useCookie } from "react-use" ;const Demo = () => { const [value, updateCookie, deleteCookie] = useCookie("my-cookie" ); const [counter, setCounter] = useState(1 ); useEffect(() => { deleteCookie(); }, []); const updateCookieHandler = () => { updateCookie(`my-awesome-cookie-${counter} ` ); setCounter(c => c + 1 ); }; return ( <div> <p>Value: {value}</p> <button onClick={updateCookieHandler}>Update Cookie</ button> <br /> <button onClick={deleteCookie}>Delete Cookie</button> </ div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import { useState, useCallback } from 'react' ;import Cookies from 'js-cookie' ;const useCookie = ( cookieName: string ): [string | null , (newValue: string, options?: Cookies.CookieAttributes) => void , () => void ] => { const [value, setValue] = useState<string | null >(( ) => Cookies.get(cookieName) || null ); const updateCookie = useCallback( (newValue: string, options?: Cookies.CookieAttributes) => { Cookies.set(cookieName, newValue, options); setValue(newValue); }, [cookieName] ); const deleteCookie = useCallback(() => { Cookies.remove(cookieName); setValue(null ); }, [cookieName]); return [value, updateCookie, deleteCookie]; }; export default useCookie;
1.核心的cookie操作是基于js-cookie 这个封装库
2.通过useState控制cookie的取值状态
用于localStorage的处理。
语法:1 2 3 4 5 const [value, setValue, remove] = useLocalStorage(key, initialValue, { raw: false, serializer: (value: T) => string, deserializer: (value: string) => T, });
参数:
key:{String},storage标识
initialValue:{any},storage值
options:参数,可选
raw:{Boolean},是否自定义加工处理。默认false
,为JSON.stringify/JSON.parse。
serializer:{Function},当options.raw
为true
时生效,设置storage值的加工函数
deserializer:{Function},当options.raw
为true
时生效,读取storage值的加工函数
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { useLocalStorage } from 'react-use' ;const Demo = () => { const [value, setValue, remove] = useLocalStorage('my-key' , 'foo' ); return ( <div> <div>Value: {value}</div> <button onClick={() => setValue('bar')}>bar</ button> <button onClick={() => setValue('baz' )}>baz</button> <button onClick={() => remove()}>Remove</ button> </div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 import { useState, useCallback, Dispatch, SetStateAction } from 'react' ;import { isClient } from './util' ;type parserOptions<T> = | { raw: true ; } | { raw: false ; serializer: (value: T ) => string; deserializer: (value: string ) => T; }; const noop = () => {};const useLocalStorage = <T > ( key: string, initialValue?: T, options?: parserOptions<T> ): [T | undefined, Dispatch<SetStateAction<T | undefined>>, () => void] => { if (!isClient) { return [initialValue as T, noop, noop]; } if (!key) { throw new Error('useLocalStorage key may not be falsy'); } const deserializer = options ? (options.raw ? value => value : options.deserializer) : JSON.parse; const [state, setState] = useState<T | undefined>(() => { try { const serializer = options ? (options.raw ? String : options.serializer) : JSON.stringify; const localStorageValue = localStorage.getItem(key); if (localStorageValue !== null) { return deserializer(localStorageValue); } else { initialValue && localStorage.setItem(key, serializer(initialValue)); return initialValue; } } catch { // If user is in private mode or has storage restriction // localStorage can throw. JSON.parse and JSON.stringify // can throw, too. return initialValue; } }); const set: Dispatch<SetStateAction<T | undefined>> = useCallback( valOrFunc => { try { const newState = typeof valOrFunc === 'function' ? (valOrFunc as Function)(state) : valOrFunc; if (typeof newState === 'undefined') return; let value: string; if (options) if (options.raw) if (typeof newState === 'string') value = newState; else value = JSON.stringify(newState); else if (options.serializer) value = options.serializer(newState); else value = JSON.stringify(newState); else value = JSON.stringify(newState); localStorage.setItem(key, value); setState(deserializer(value)); } catch { // If user is in private mode or has storage restriction // localStorage can throw. Also JSON.stringify can throw. } }, [key, setState] ); const remove = useCallback(() => { try { localStorage.removeItem(key); setState(undefined); } catch { // If user is in private mode or has storage restriction // localStorage can throw. } }, [key, setState]); return [state, set, remove]; }; export default useLocalStorage;
1.对非浏览器环境(用window变量判断)做了处理,直接返回原值,操作函数为空函数;
serializer/deserializer处理函数用于hook state调用,不影响localStorage的读写;
3.参数raw为false时,默认设置storage操作时会执行JSON.stringify(val)处理,读取storage操作时会执行JSON.parse(val)处理;
4.参数raw为true时,serializer默认为String,即设置storage操作时执行String(val)处理;deserializer默认为value => value
,即读取storage操作时会直接返回;
用于sessionStorage的处理。
语法:1 2 3 const [value, setValue] = useSessionStorage(key, initialValue, { raw: false, });
参数:
key:{String},storage标识
initialValue:{any},storage值
options:参数,可选
raw:{Boolean},是否自定义加工处理。默认false
,为JSON.stringify/JSON.parse。
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 import {useSessionStorage} from 'react-use' ;const Demo = () => { const [value, setValue] = useSessionStorage('my-key' , 'foo' ); return ( <div> <div>Value: {value}</div> <button onClick={() => setValue('bar')}>bar</ button> <button onClick={() => setValue('baz' )}>baz</button> </ div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import { useEffect, useState } from 'react' ;import { isClient } from './util' ;const useSessionStorage = <T > (key: string, initialValue?: T, raw?: boolean): [T, (value: T) => void] => { if (!isClient) { return [initialValue as T, () => {}]; } const [state, setState] = useState<T>(() => { try { const sessionStorageValue = sessionStorage.getItem(key); if (typeof sessionStorageValue !== 'string') { sessionStorage.setItem(key, raw ? String(initialValue) : JSON.stringify(initialValue)); return initialValue; } else { return raw ? sessionStorageValue : JSON.parse(sessionStorageValue || 'null'); } } catch { // If user is in private mode or has storage restriction // sessionStorage can throw. JSON.parse and JSON.stringify // cat throw, too. return initialValue; } }); useEffect(() => { try { const serializedState = raw ? String(state) : JSON.stringify(state); sessionStorage.setItem(key, serializedState); } catch { // If user is in private mode or has storage restriction // sessionStorage can throw. Also JSON.stringify can throw. } }); return [state, setState]; }; export default useSessionStorage;
1.对非浏览器环境(用window变量判断)做了处理,直接返回原值,操作函数为空函数;
2.参数raw为false时,默认设置storage操作时会执行JSON.stringify(val)处理,读取storage操作时会执行JSON.parse(val)处理;
3.sessionStorage的应用场景相对localStorage较小,因此无需发布订阅的控制
控制requestAnimationFrame循环。
语法:1 const [stopLoop, startLoop, isActive] = useRafLoop(callback: FrameRequestCallback, initiallyActive = true);
参数:
callback:{Function},RAF片段的控制回调函数
initiallyActive: {boolean},是否已开始就激活循环,默认为true
。
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import * as React from 'react' ;import { useRafLoop, useUpdate } from 'react-use' ;const Demo = () => { const [ticks, setTicks] = React.useState(0 ); const [lastCall, setLastCall] = React.useState(0 ); const update = useUpdate(); const [loopStop, loopStart, isActive] = useRafLoop((time ) => { setTicks(ticks => ticks + 1 ); setLastCall(time); }); return ( <div> <div>RAF triggered: {ticks} (times)</div> <div>Last high res timestamp: {lastCall}</ div> <br /> <button onClick={() => { isActive() ? loopStop() : loopStart(); update(); }}>{isActive() ? 'STOP' : 'START' }</button> </ div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import { useCallback, useEffect, useMemo, useRef } from 'react' ;export type RafLoopReturns = [() => void , () => void , () => boolean];export default function useRafLoop (callback: FrameRequestCallback, initiallyActive = true ): RafLoopReturns { const raf = useRef<number | null >(null ); const rafActivity = useRef<boolean>(false ); const rafCallback = useRef(callback); rafCallback.current = callback; const step = useCallback((time: number ) => { if (rafActivity.current) { rafCallback.current(time); raf.current = requestAnimationFrame(step); } }, []); const result = useMemo(() => ([ () => { if (rafActivity.current) { rafActivity.current = false ; raf.current && cancelAnimationFrame(raf.current); } }, () => { if (!rafActivity.current) { rafActivity.current = true ; raf.current = requestAnimationFrame(step); } }, (): boolean => rafActivity.current ] as RafLoopReturns), []); useEffect(() => { if (initiallyActive) { result[1 ](); } return result[0 ]; }, []); return result; }
1.用ref来缓存raf控制器;
2.用useMemo()来缓存控制方法;
3.组件销毁时会取消raf;
抛出异常控制。
语法:1 const dispatchError = useError();
参数:无
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { useError } from 'react-use' ;const Demo = () => { const dispatchError = useError(); const clickHandler = () => { dispatchError(new Error ('Some error!' )); }; return <button onClick ={clickHandler} > Click me to throw</button > ; }; const App = () => ( <ErrorBoundary> <Demo /> </ErrorBoundary> );
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { useState, useEffect, useCallback } from 'react' ;const useError = () : ((err: Error ) => void ) => { const [error, setError] = useState<Error | null >(null ); useEffect(() => { if (error) { throw error; } }, [error]); const dispatchError = useCallback((err: Error ) => { setError(err); }, []); return dispatchError; }; export default useError;
throw异常。
设置浏览器标题图标。
语法:
参数:
使用:1 2 3 4 5 6 7 import {useFavicon} from 'react-use' ;const Demo = () => { useFavicon('https://cdn.sstatic.net/Sites/stackoverflow/img/favicon.ico' ); return null ; };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { useEffect } from 'react' ;const useFavicon = (href: string ) => { useEffect(() => { const link: HTMLLinkElement = document .querySelector("link[rel*='icon']" ) || document .createElement('link' ); link.type = 'image/x-icon' ; link.rel = 'shortcut icon' ; link.href = href; document .getElementsByTagName('head' )[0 ].appendChild(link); }, [href]); }; export default useFavicon;
简单来说,就是设置属性是icon的link标签的引用地址。
浏览器API兼容状态的信息获取(基于navigator.permissions )
语法:1 const state = usePermission({ name: 'microphone' });
参数:
使用:1 2 3 4 5 6 7 8 9 10 11 import {usePermission} from 'react-use' ;const Demo = () => { const state = usePermission({ name : 'microphone' }); return ( <pre> {JSON .stringify(state, null , 2 )} </pre> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import { useEffect, useState } from 'react' ;import { off, on } from './util' ;type PermissionDesc = | PermissionDescriptor | DevicePermissionDescriptor | MidiPermissionDescriptor | PushPermissionDescriptor; type State = PermissionState | '' ; const noop = () => {};const usePermission = (permissionDesc: PermissionDesc): State => { let mounted = true ; let permissionStatus: PermissionStatus | null = null ; const [state, setState] = useState<State>('' ); const onChange = () => { if (mounted && permissionStatus) { setState(permissionStatus.state); } }; const changeState = () => { onChange(); on(permissionStatus, 'change' , onChange); }; useEffect(() => { navigator.permissions .query(permissionDesc) .then(status => { permissionStatus = status; changeState(); }) .catch(noop); return () => { mounted = false ; permissionStatus && off(permissionStatus, 'change' , onChange); }; }, []); return state; }; export default usePermission;
核心还是navigator.permissions。不过由于兼容问题,基本不会用。
页面将退出/即将重新加载时触发。
语法:1 useBeforeUnload(enabled, message);
参数:
enabled:{Boolean},是否启用
message:{String},退出/将重启加载时的提示文案
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import {useBeforeUnload} from 'react-use' ;const Demo = () => { const [dirty, toggleDirty] = useToggle(false ); useBeforeUnload(dirty, 'You have unsaved changes, are you sure?' ); return ( <div> {dirty && <p > Try to reload or close tab</p > } <button onClick={() => toggleDirty()}>{dirty ? 'Disable' : 'Enable' }</button> </ div> ); };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import {useBeforeUnload} from 'react-use' ;const Demo = () => { const [dirty, toggleDirty] = useToggle(false ); const dirtyFn = useCallback(() => { return dirty; }, [dirty]); useBeforeUnload(dirtyFn, 'You have unsaved changes, are you sure?' ); return ( <div> {dirty && <p > Try to reload or close tab</p > } <button onClick={() => toggleDirty()}>{dirty ? 'Disable' : 'Enable' }</button> </ div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import { useCallback, useEffect } from 'react' ;const useBeforeUnload = (enabled: boolean | (( ) => boolean) = true , message?: string) => { const handler = useCallback( (event: BeforeUnloadEvent) => { const finalEnabled = typeof enabled === 'function' ? enabled() : true ; if (!finalEnabled) { return ; } event.preventDefault(); if (message) { event.returnValue = message; } return message; }, [enabled, message] ); useEffect(() => { if (!enabled) { return ; } window .addEventListener('beforeunload' , handler); return () => window .removeEventListener('beforeunload' , handler); }, [enabled, handler]); }; export default useBeforeUnload;
1.核心基于beforeunload的事件和解除;
2.enabled默认为true
,当传入函数时则获取函数执行结果,闭包的作用。;
3.考虑了IE的event.returnValue用于事件捕获。
beforeunload事件的兼容情况:
控制区域滚动条是否可以滚动。
语法:1 useLockBodyScroll(locked = true, elementRef);
参数:
locked:{Boolean},是否锁定滚动,默认为true
;
elementRef:{DOMElement},滚动区域元素
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import {useLockBodyScroll, useToggle} from 'react-use' ;const Demo = () => { const [locked, toggleLocked] = useToggle(false ) useLockBodyScroll(locked); return ( <div> <button onClick={() => toggleLocked()}> {locked ? 'Unlock' : 'Lock' } </button> </ div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 import { RefObject, useEffect, useRef } from 'react' ;export function getClosestBody (el: Element | HTMLElement | HTMLIFrameElement | null ): HTMLElement | null { if (!el) { return null ; } else if (el.tagName === 'BODY' ) { return el as HTMLElement; } else if (el.tagName === 'IFRAME' ) { const document = (el as HTMLIFrameElement).contentDocument; return document ? document .body : null ; } else if (!(el as HTMLElement).offsetParent) { return null ; } return getClosestBody((el as HTMLElement).offsetParent!); } function preventDefault (rawEvent: TouchEvent ): boolean { const e = rawEvent || window .event; if (e.touches.length > 1 ) return true ; if (e.preventDefault) e.preventDefault(); return false ; } export interface BodyInfoItem { counter: number; initialOverflow: CSSStyleDeclaration['overflow' ]; } const isIosDevice = typeof window !== 'undefined' && window .navigator && window .navigator.platform && /iP(ad|hone|od)/.test(window .navigator.platform); const bodies: Map <HTMLElement, BodyInfoItem> = new Map ();const doc: Document | undefined = typeof document === 'object' ? document : undefined ;let documentListenerAdded = false ;export default !doc ? function useLockBodyMock (_locked: boolean = true, _elementRef?: RefObject<HTMLElement> ) {} : function useLockBody (locked: boolean = true, elementRef?: RefObject<HTMLElement> ) { elementRef = elementRef || useRef(doc!.body); const lock = body => { const bodyInfo = bodies.get(body); if (!bodyInfo) { bodies.set(body, { counter : 1 , initialOverflow : body.style.overflow }); if (isIosDevice) { if (!documentListenerAdded) { document .addEventListener('touchmove' , preventDefault, { passive : false }); documentListenerAdded = true ; } } else { body.style.overflow = 'hidden' ; } } else { bodies.set(body, { counter : bodyInfo.counter + 1 , initialOverflow : bodyInfo.initialOverflow }); } }; const unlock = body => { const bodyInfo = bodies.get(body); if (bodyInfo) { if (bodyInfo.counter === 1 ) { bodies.delete(body); if (isIosDevice) { body.ontouchmove = null ; if (documentListenerAdded) { document .removeEventListener('touchmove' , preventDefault); documentListenerAdded = false ; } } else { body.style.overflow = bodyInfo.initialOverflow; } } else { bodies.set(body, { counter : bodyInfo.counter - 1 , initialOverflow : bodyInfo.initialOverflow }); } } }; useEffect(() => { const body = getClosestBody(elementRef!.current); if (!body) { return ; } if (locked) { lock(body); } else { unlock(body); } }, [locked, elementRef.current]); useEffect(() => { const body = getClosestBody(elementRef!.current); if (!body) { return ; } return () => { unlock(body); }; }, []); };
1.<body>
和<iframe>
元素会默认本身为滚动元素,其他元素则会向上层递归寻找滚动元素,这也是为何官方会推荐使用<body>
或<iframe>
作为控制元素的原因,因为它们没有parent;
2.滚动的锁定控制会根据平台来区分处理,iOS是通过touchmove事件来控制,而其他平台是通过设置元素的overflow样式属性控制;
用户剪切板的拷贝控制。
语法:1 const [{value, error, noUserInteraction}, copyToClipboard] = useCopyToClipboard();
参数:无
返回:
dataObj:{Object},返回字段
value:{String},已经被拷贝的值;
error:{String},是否被捕获异常;
noUserInteraction:{Boolean},用户交互拷贝的值是否暴露给copy-to-clipboard
这个库
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 const Demo = () => { const [text, setText] = React.useState('' ); const [state, copyToClipboard] = useCopyToClipboard(); return ( <div> <input value={text} onChange={e => setText(e.target.value)} /> <button type="button" onClick={() => copyToClipboard(text)}>copy text</button> {state.error ? <p>Unable to copy value: {state.error.message}</ p> : state.value && <p > Copied {state.value}</p > } </div> ) }
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 import writeText from 'copy-to-clipboard' ;import { useCallback } from 'react' ;import useMountedState from './useMountedState' ;import useSetState from './useSetState' ;export interface CopyToClipboardState { value?: string; noUserInteraction: boolean; error?: Error ; } const useCopyToClipboard = () : [CopyToClipboardState , (value: string ) => void ] => { const isMounted = useMountedState(); const [state, setState] = useSetState<CopyToClipboardState>({ value: undefined , error: undefined , noUserInteraction: true , }); const copyToClipboard = useCallback(value => { if (!isMounted()) { return ; } let noUserInteraction; let normalizedValue; try { if (typeof value !== 'string' && typeof value !== 'number' ) { const error = new Error (`Cannot copy typeof ${typeof value} to clipboard, must be a string` ); if (process.env.NODE_ENV === 'development' ) console .error(error); setState({ value, error, noUserInteraction: true , }); return ; } else if (value === '' ) { const error = new Error (`Cannot copy empty string to clipboard.` ); if (process.env.NODE_ENV === 'development' ) console .error(error); setState({ value, error, noUserInteraction: true , }); return ; } normalizedValue = value.toString(); noUserInteraction = writeText(normalizedValue); setState({ value: normalizedValue, error: undefined , noUserInteraction, }); } catch (error) { setState({ value: normalizedValue, error, noUserInteraction, }); } }, []); return [state, copyToClipboard]; }; export default useCopyToClipboard;
2.捕获异常通过try...catch...
3.useMountedState()
也是react-use提供的一个钩子函数,用于获取组件mount状态信息;
防抖。
语法:1 2 3 4 const [ isReady: () => boolean | null, cancel: () => void, ] = useDebounce(fn: Function, ms: number, deps: DependencyList = []);
参数:
fn:{Function},回调函数;
ms:{Number},延迟时间,毫秒;
deps:{Array},依赖列表,等同于useEffect的第二个参数,可选;
返回:
isReady:()=> boolean|null
。当前防抖函数的执行状态。
false
:pending
true
:called
null
:cancelled
cancel: ()=>void
。取消执行
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const Demo = () => { const [state, setState] = React.useState('Typing stopped' ); const [val, setVal] = React.useState('' ); const [debouncedValue, setDebouncedValue] = React.useState('' ); const [, cancel] = useDebounce( () => { setState('Typing stopped' ); setDebouncedValue(val); }, 2000 , [val] ); return ( <div> <input type="text" value={val} placeholder="Debounced input" onChange={({ currentTarget }) => { setState('Waiting for typing to stop...' ); setVal(currentTarget.value); }} /> <div>{state}</div> <div> Debounced value: {debouncedValue} <button onClick={cancel}>Cancel debounce</ button> </div> </ div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { DependencyList, useEffect } from 'react' ;import useTimeoutFn from './useTimeoutFn' ;export type UseDebounceReturn = [() => boolean | null , () => void ];export default function useDebounce (fn: Function, ms: number = 0 , deps: DependencyList = [] ): UseDebounceReturn { const [isReady, cancel, reset] = useTimeoutFn(fn, ms); useEffect(reset, deps); return [isReady, cancel]; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import { useCallback, useEffect, useRef } from 'react' ;export type UseTimeoutFnReturn = [() => boolean | null , () => void , () => void ];export default function useTimeoutFn (fn: Function, ms: number = 0 ): UseTimeoutFnReturn { const ready = useRef<boolean | null >(false ); const timeout = useRef<ReturnType<typeof setTimeout>>(); const callback = useRef(fn); const isReady = useCallback(() => ready.current, []); const set = useCallback(() => { ready.current = false ; timeout.current && clearTimeout(timeout.current); timeout.current = setTimeout(() => { ready.current = true ; callback.current(); }, ms); }, [ms]); const clear = useCallback(() => { ready.current = null ; timeout.current && clearTimeout(timeout.current); }, []); useEffect(() => { callback.current = fn; }, [fn]); useEffect(() => { set (); return clear; }, [ms]); return [isReady, clear, set ]; }
1.定时器单独封装成了一个工具hook:useTimeoutFn;
2.useTimeoutFn中用useRef来缓存定时器返回的定时器id,避免hook的作用域副作用;
节流。
语法:1 2 useThrottle(value, ms?: number); useThrottleFn(fn, ms, args);
参数:
value:{any},节流的内容
ms:{Number},节流延迟时间,单位为毫秒
args:{Array},依赖列表,等同于useEffect的第二个参数,可选;
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 import React, { useState } from 'react' ;import { useThrottle, useThrottleFn } from 'react-use' ;const Demo = ({value} ) => { const throttledValue = useThrottle(value); return ( <> <div>Value: {value}</div> <div>Throttled value: {throttledValue}</ div> </> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import { useEffect, useRef, useState } from 'react' ;import useUnmount from './useUnmount' ;const useThrottle = <T > (value: T, ms: number = 200) => { const [state, setState] = useState<T>(value); const timeout = useRef<ReturnType<typeof setTimeout>>(); const nextValue = useRef(null) as any; const hasNextValue = useRef(0) as any; useEffect(() => { if (!timeout.current) { setState(value); const timeoutCallback = () => { if (hasNextValue.current) { hasNextValue.current = false; setState(nextValue.current); timeout.current = setTimeout(timeoutCallback, ms); } else { timeout.current = undefined; } }; timeout.current = setTimeout(timeoutCallback, ms); } else { nextValue.current = value; hasNextValue.current = true; } }, [value]); useUnmount(() => { timeout.current && clearTimeout(timeout.current); }); return state; }; export default useThrottle;
useUnmount主要实现了组件即将销毁时的回调,在useThrottleFn中主要作用是触发组件销毁时的定时器取消;
useThrottle较为简单,用useEffect来控制节流触发,以value参数作为控制变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import { useEffect, useRef, useState } from 'react' ;import useUnmount from './useUnmount' ;const useThrottleFn = <T, U extends any []> (fn: (...args: U) => T, ms: number = 200, args: U) => { const [state, setState] = useState<T | null>(null); const timeout = useRef<ReturnType<typeof setTimeout>>(); const nextArgs = useRef<U>(); useEffect(() => { if (!timeout.current) { setState(fn(...args)); const timeoutCallback = () => { if (nextArgs.current) { setState(fn(...nextArgs.current)); nextArgs.current = undefined; timeout.current = setTimeout(timeoutCallback, ms); } else { timeout.current = undefined; } }; timeout.current = setTimeout(timeoutCallback, ms); } else { nextArgs.current = args; } }, args); useUnmount(() => { timeout.current && clearTimeout(timeout.current); }); return state; }; export default useThrottleFn;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { useRef } from 'react' ;import useEffectOnce from './useEffectOnce' ;const useUnmount = (fn: ( ) => any): void => { const fnRef = useRef(fn); fnRef.current = fn; useEffectOnce(() => () => fnRef.current()); }; export default useUnmount;
1 2 3 4 5 6 7 8 import { EffectCallback, useEffect } from 'react' ;const useEffectOnce = (effect: EffectCallback ) => { useEffect(effect, []); }; export default useEffectOnce;
useThrottleFn与useThrottle类似,主要控制了函数的触发。
异步方法钩子。
语法:1 const [state, fetch] = useAsyncFn<Result, Args>(fn, deps?: any[], initialState?: AsyncState<Result>);
参数:
fn:{Function},异步函数
args:{Array},依赖列表,等同于useEffect的第二个参数,可选;
initialState:{any},初始值
返回:
state:{Object}
loading:{Boolean},是否处于pending
error:{Error},是否抛出了异常
value:{String},返回值
fetch:开始执行方法
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import {useAsyncFn} from 'react-use' ;const Demo = ({url} ) => { const [state, fetch] = useAsyncFn(async () => { const response = await fetch(url); const result = await response.text(); return result }, [url]); return ( <div> {state.loading ? <div > Loading...</div > : state.error ? <div > Error: {state.error.message}</div > : <div > Value: {state.value}</div > } <button onClick={() => fetch()}>Start loading</button> </ div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import { DependencyList, useCallback, useState, useRef } from 'react' ;import useMountedState from './useMountedState' ;import { FnReturningPromise, PromiseType } from './util' ;export type AsyncState<T> = | { loading: boolean; error?: undefined ; value?: undefined ; } | { loading: true ; error?: Error | undefined ; value?: T; } | { loading: false ; error: Error ; value?: undefined ; } | { loading: false ; error?: undefined ; value: T; }; type StateFromFnReturningPromise<T extends FnReturningPromise> = AsyncState<PromiseType<ReturnType<T>>>; export type AsyncFnReturn<T extends FnReturningPromise = FnReturningPromise> = [StateFromFnReturningPromise<T>, T];export default function useAsyncFn <T extends FnReturningPromise >( fn: T, deps: DependencyList = [], initialState: StateFromFnReturningPromise<T> = { loading: false } ): AsyncFnReturn <T > { const lastCallId = useRef(0 ); const isMounted = useMountedState(); const [state, set ] = useState<StateFromFnReturningPromise<T>>(initialState); const callback = useCallback((...args: Parameters<T>): ReturnType<T> => { const callId = ++lastCallId.current; set (prevState => ({ ...prevState, loading : true })); return fn(...args).then( value => { isMounted() && callId === lastCallId.current && set ({ value, loading : false }); return value; }, error => { isMounted() && callId === lastCallId.current && set ({ error, loading : false }); return error; } ) as ReturnType<T>; }, deps); return [state, (callback as unknown) as T]; }
从util库中引用的FnReturningPromise, PromiseType只是对类型的定义声明;
异步钩子。
语法:1 const state = useAsync(fn, args?: any[]);
参数:
fn:{Function},异步函数
args:{Array},依赖列表,等同于useEffect的第二个参数,可选;
返回:
state:{Object}
loading:{Boolean},是否处于pending
error:{Error},是否抛出了异常
value:{String},返回值
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import {useAsync} from 'react-use' ;const Demo = ({url} ) => { const state = useAsync(async () => { const response = await fetch(url); const result = await response.text(); return result }, [url]); return ( <div> {state.loading ? <div > Loading...</div > : state.error ? <div > Error: {state.error.message}</div > : <div > Value: {state.value}</div > } </div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { DependencyList, useEffect } from 'react' ;import useAsyncFn from './useAsyncFn' ;import { FnReturningPromise } from './util' ;export { AsyncState, AsyncFnReturn } from './useAsyncFn' ;export default function useAsync <T extends FnReturningPromise >(fn: T, deps: DependencyList = [] ) { const [state, callback] = useAsyncFn(fn, deps, { loading: true , }); useEffect(() => { callback(); }, [callback]); return state; }
就是在useAsyncFn之上做了一层封装。
带取消功能的异步钩子。
语法:1 const state = useAsyncRetry(fn, args?: any[]);
参数:
fn:{Function},异步函数
args:{Array},依赖列表,等同于useEffect的第二个参数,可选;
返回:
state:{Object}
loading:{Boolean},是否处于pending
error:{Error},是否抛出了异常
value:{String},返回值
retry:{Function},取消方法
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import {useAsyncRetry} from 'react-use' ;const Demo = ({url} ) => { const state = useAsyncRetry(async () => { const response = await fetch(url); const result = await response.text(); return result; }, [url]); return ( <div> {state.loading ? <div > Loading...</div > : state.error ? <div > Error: {state.error.message}</div > : <div > Value: {state.value}</div > } {!loading && <button onClick ={() => state.retry()}>Start loading</button > } </div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import { DependencyList, useCallback, useState } from 'react' ;import useAsync, { AsyncState } from './useAsync' ;export type AsyncStateRetry<T> = AsyncState<T> & { retry(): void ; }; const useAsyncRetry = <T>(fn: () => Promise<T>, deps: DependencyList = []) => { const [attempt, setAttempt] = useState<number>(0); const state = useAsync(fn, [...deps, attempt]); const stateLoading = state.loading; const retry = useCallback(() => { if (stateLoading) { if (process.env.NODE_ENV === 'development') { console.log('You are calling useAsyncRetry hook retry() method while loading in progress, this is a no-op.'); } return; } setAttempt(currentAttempt => currentAttempt + 1); }, [...deps, stateLoading]); return { ...state, retry }; }; export default useAsyncRetry;
基于useAsync的封装,通过封装回调方法来控制退出方法。
生命周期 只执行一次的effect钩子。
语法:1 useEffectOnce(effect: EffectCallback);
参数:
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 import {useEffectOnce} from 'react-use' ;const Demo = () => { useEffectOnce(() => { console .log('Running effect once on mount' ) return () => { console .log('Running clean-up of effect on unmount' ) } }); return null ; };
源码及解析 1 2 3 4 5 6 7 import { EffectCallback, useEffect } from 'react' ;const useEffectOnce = (effect: EffectCallback ) => { useEffect(effect, []); }; export default useEffectOnce;
不需要解释。。。
事件处理。
语法:
1 2 useEvent(event, handler) useEvent(event, handler, target, options)
参数:
event:{String},事件名称;
handler:{function},事件函数;
target:{DOMElement|Window},事件节点;
options:{Object},参数。一个指定有关 listener 属性的可选参数对象。可用的选项如下:
capture:{Boolean},表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。
once:{Boolean},表示 listener 在添加之后最多只调用一次。如果是 true, listener 会在其被调用之后自动移除。
passive:{Boolean},设置为true时,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。查看 使用 passive 改善的滚屏性能 了解更多。
mozSystemGroup: {Boolean},只能在 XBL 或者是 Firefox’ chrome 使用,表示 listener 被添加到 system group。
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import {useEvent, useList} from 'react-use' ;const Demo = () => { const [list, {push, clear}] = useList(); const onKeyDown = useCallback(({key} ) => { if (key === 'r' ) clear(); push(key); }, []); useEvent('keydown' , onKeyDown); return ( <div> <p> Press some keys on your keyboard, <code style={{color : 'tomato' }}>r</code> key resets the list </ p> <pre> {JSON .stringify(list, null , 4 )} </pre> </ div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import { useEffect } from 'react' ;import { isClient } from './util' ;export interface ListenerType1 { addEventListener(name: string, handler : (event?: any ) => void , ...args: any[]); removeEventListener(name: string, handler : (event?: any ) => void , ...args: any[]); } export interface ListenerType2 { on(name: string, handler : (event?: any ) => void , ...args: any[]); off(name: string, handler : (event?: any ) => void , ...args: any[]); } export type UseEventTarget = ListenerType1 | ListenerType2;const defaultTarget = isClient ? window : null ;const isListenerType1 = (target: any): target is ListenerType1 => { return !!target.addEventListener; }; const isListenerType2 = (target: any): target is ListenerType2 => { return !!target.on; }; type AddEventListener<T> = T extends ListenerType1 ? T['addEventListener' ] : T extends ListenerType2 ? T['on' ] : never; const useEvent = <T extends UseEventTarget > ( name: Parameters<AddEventListener<T>>[0], handler?: null | undefined | Parameters<AddEventListener<T>>[1], target: null | T | Window = defaultTarget, options?: Parameters<AddEventListener<T>>[2] ) => { useEffect(() => { if (!handler) { return; } if (!target) { return; } if (isListenerType1(target)) { target.addEventListener(name, handler, options); } else if (isListenerType2(target)) { target.on(name, handler, options); } return () => { if (isListenerType1(target)) { target.removeEventListener(name, handler, options); } else if (isListenerType2(target)) { target.off(name, handler, options); } }; }, [name, handler, target, JSON.stringify(options)]); }; export default useEvent;
1.考虑了IE的事件处理;
2.考虑了非浏览器端的兼容;
mount和unmount的生命周期封装。
语法:1 useLifecycles(mount, unmount);
参数:
mount:{Function},mount执行回调
unmount:{Function},unmount执行回调
使用:1 2 3 4 5 6 import {useLifecycles} from 'react-use' ;const Demo = () => { useLifecycles(() => console .log('MOUNTED' ), () => console .log('UNMOUNTED' )); return null ; };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { useEffect } from 'react' ;const useLifecycles = (mount, unmount? ) => { useEffect(() => { if (mount) { mount(); } return () => { if (unmount) { unmount(); } }; }, []); }; export default useLifecycles;
不需要解释。。。
组件状态回调钩子。
语法:1 const isMounted = useMountedState();
返回:
isMounted:{Function},() => boolean
,获取当前是否mount状态
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import * as React from 'react' ;import {useMountedState} from 'react-use' ;const Demo = () => { const isMounted = useMountedState(); React.useEffect(() => { setTimeout(() => { if (isMounted()) { } else { } }, 1000 ); }); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { useCallback, useEffect, useRef } from 'react' ;export default function useMountedState ( ): ( ) => boolean { const mountedRef = useRef<boolean>(false ); const get = useCallback(() => mountedRef.current, []); useEffect(() => { mountedRef.current = true ; return () => { mountedRef.current = false ; }; }); return get ; }
通过useEffect来控制mount状态,用ref防止作用域问题。
语法:
1 2 3 const mounted = useUnmountPromise(); mounted(promise, onError);
返回:
mounted:{Function},执行函数。参数:
promise:{Function},回调函数
onError:{Function},异常捕获处理,可选
使用:1 2 3 4 5 6 7 8 import useUnmountPromise from 'react-use/lib/useUnmountPromise' ;const Demo = () => { const mounted = useUnmountPromise(); useEffect(async () => { await mounted(someFunction()); }); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import { useMemo, useRef, useEffect } from 'react' ;export type Race = <P extends Promise<any>, E = any>(promise: P, onError?: (error: E) => void) => P;const useUnmountPromise = (): Race => { const refUnmounted = useRef(false); useEffect(() => () => { refUnmounted.current = true; }); const wrapper = useMemo(() => { const race = <P extends Promise<any>, E>(promise: P, onError?: (error: E) => void) => { const newPromise: P = new Promise((resolve, reject) => { promise.then( result => { if (!refUnmounted.current) resolve(result); }, error => { if (!refUnmounted.current) reject(error); else if (onError) onError(error); else console.error('useUnmountPromise', error); } ); }) as P; return newPromise; }; return race; }, []); return wrapper; }; export default useUnmountPromise;
promise的处理+ref的缓存。
用于处理函数组件中的包裹promise方法。并且此方法只有当前组件mount后才会resolve。
语法:1 const mounted = usePromise();
返回:
mounted:{Function},包裹函数。参数:
promise:{Function},promise方法
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 import {usePromise} from 'react-use' ;const Demo = ({promise} ) => { const mounted = usePromise(); const [value, setValue] = useState(); useEffect(() => { (async () => { const value = await mounted(promise); setValue(value); })(); }); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { useCallback } from 'react' ;import useMountedState from './useMountedState' ;export type UsePromise = () => <T>(promise: Promise<T>) => Promise<T>;const usePromise: UsePromise = () => { const isMounted = useMountedState(); return useCallback( (promise: Promise<any>) => new Promise<any>((resolve, reject) => { const onValue = value => { isMounted() && resolve(value); }; const onError = error => { isMounted() && reject(error); }; promise.then(onValue, onError); }), [] ); }; export default usePromise;
简单根据当前组件跟mount状态来进行控制。
包含当前生命周期状态的log控制。
语法:1 useLogger(componentName: string, ...rest);
参数:
componentName:{String},log内容
使用:1 2 3 4 5 6 import {useLogger} from 'react-use' ;const Demo = (props ) => { useLogger('Demo' , props); return null ; };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import useEffectOnce from './useEffectOnce' ;import useUpdateEffect from './useUpdateEffect' ;const useLogger = (componentName: string, ...rest ) => { useEffectOnce(() => { console .log(`${componentName} mounted` , ...rest); return () => console .log(`${componentName} unmounted` ); }); useUpdateEffect(() => { console .log(`${componentName} updated` , ...rest); }); }; export default useLogger;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { useEffect } from 'react' ;import { useFirstMountState } from './useFirstMountState' ;const useUpdateEffect: typeof useEffect = (effect, deps ) => { const isFirstMount = useFirstMountState(); useEffect(() => { if (!isFirstMount) { return effect(); } }, deps); }; export default useUpdateEffect;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { useRef } from 'react' ;export function useFirstMountState ( ): boolean { const isFirst = useRef(true ); if (isFirst.current) { isFirst.current = false ; return true ; } return isFirst.current; }
1.useUpdateEffect是通过useFirstMountState的闭包变量进行的判断,实现控制非首次触发effect的效果(update);
2.useEffectOnce用于实现mount和unmount的控制回调;
语法:1 useMount(fn: () => void);
参数:
使用:1 2 3 4 5 6 import {useMount} from 'react-use' ;const Demo = () => { useMount(() => alert('MOUNTED' )); return null ; };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 import useEffectOnce from './useEffectOnce' ;const useMount = (fn: ( ) => void ) => { useEffectOnce(() => { fn(); }); }; export default useMount;
不需要解释。。。
组件更新时的effect。
语法:1 useUpdateEffect(fn, deps)
参数:
fn:{Function},回调函数;
deps:[Array],依赖项;
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import React from 'react' import {useUpdateEffect} from 'react-use' ;const Demo = () => { const [count, setCount] = React.useState(0 ); React.useEffect(() => { const interval = setInterval(() => { setCount(count => count + 1 ) }, 1000 ) return () => { clearInterval(interval) } }, []) useUpdateEffect(() => { console .log('count' , count) return () => { } }) return <div > Count: {count}</div > };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { useEffect } from 'react' ;import { useFirstMountState } from './useFirstMountState' ;const useUpdateEffect: typeof useEffect = (effect, deps ) => { const isFirstMount = useFirstMountState(); useEffect(() => { if (!isFirstMount) { return effect(); } }, deps); }; export default useUpdateEffect;
useUpdateEffect是通过useFirstMountState的闭包变量进行的判断,实现控制非首次触发effect的效果(update)
根据环境判断使用layoutEffect/effect。
语法:1 useIsomorphicLayoutEffect(effect: EffectCallback, deps?: ReadonlyArray<any> | undefined);
参数:
effect:{Function},回调
deps:{Array},依赖列表,等同于useEffect的第二个参数,可选;
使用:1 2 3 4 5 6 7 8 9 import {useIsomorphicLayoutEffect} from 'react-use' ;const Demo = ({value} ) => { useIsomorphicLayoutEffect(() => { window .console.log(value) }, [value]); return null ; };
源码及解析 1 2 3 4 5 6 7 import { useEffect, useLayoutEffect } from 'react' ;const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;export default useIsomorphicLayoutEffect;
根据执行环境进行了区分使用。
通过依赖项的比较来控制effect执行。
语法:1 useCustomCompareEffect(effect: () => void | (() => void | undefined), deps: any[], depsEqual: (prevDeps: any[], nextDeps: any[]) => boolean);
effect:{Function},副作用回调
deps:{Array},依赖列表;
depsEqual:{Function},比较函数;参数
prevDeps:{Array},比较前依赖项
nextDeps:{Array},比较后依赖项; 返回:{Boolean},是否需要执行副作用
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import {useCounter, useCustomCompareEffect} from 'react-use' ;import isEqual from 'lodash/isEqual' ;const Demo = () => { const [count, {inc : inc}] = useCounter(0 ); const options = { step : 2 }; useCustomCompareEffect(() => { inc(options.step) }, [options], (prevDeps, nextDeps) => isEqual(prevDeps, nextDeps)); return ( <div> <p>useCustomCompareEffect with deep comparison: {count}</p> </ div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import { DependencyList, EffectCallback, useEffect, useRef } from 'react' ;const isPrimitive = (val: any ) => val !== Object (val);type DepsEqualFnType<TDeps extends DependencyList> = (prevDeps: TDeps, nextDeps: TDeps ) => boolean; const useCustomCompareEffect = <TDeps extends DependencyList > ( effect: EffectCallback, deps: TDeps, depsEqual: DepsEqualFnType<TDeps> ) => { if (process.env.NODE_ENV !== 'production') { if (!(deps instanceof Array) || !deps.length) { console.warn('`useCustomCompareEffect` should not be used with no dependencies. Use React.useEffect instead.'); } if (deps.every(isPrimitive)) { console.warn( '`useCustomCompareEffect` should not be used with dependencies that are all primitive values. Use React.useEffect instead.' ); } if (typeof depsEqual !== 'function') { console.warn('`useCustomCompareEffect` should be used with depsEqual callback for comparing deps list'); } } const ref = useRef<TDeps | undefined>(undefined); if (!ref.current || !depsEqual(deps, ref.current)) { ref.current = deps; } useEffect(effect, ref.current); }; export default useCustomCompareEffect;
1.用ref来控制useEffect的缓存依赖项
2.如果依赖项deps都是原始数据类型的话不推荐使用此钩子
使用深比较effect。
语法:1 useDeepCompareEffect(effect: () => void | (() => void | undefined), deps: any[]);
参数:
effect:{Function},副作用回调
deps:{Array},依赖列表,等同于useEffect的第二个参数,可选;
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import {useCounter, useDeepCompareEffect} from 'react-use' ;const Demo = () => { const [count, {inc : inc}] = useCounter(0 ); const options = { step : 2 }; useDeepCompareEffect(() => { inc(options.step) }, [options]); return ( <div> <p>useDeepCompareEffect: {count}</p> </ div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { DependencyList, EffectCallback } from 'react' ;import { isDeepEqual } from './util' ; import useCustomCompareEffect from './useCustomCompareEffect' ;const isPrimitive = (val: any ) => val !== Object (val);const useDeepCompareEffect = (effect: EffectCallback, deps: DependencyList ) => { if (process.env.NODE_ENV !== 'production' ) { if (!(deps instanceof Array ) || !deps.length) { console .warn('`useDeepCompareEffect` should not be used with no dependencies. Use React.useEffect instead.' ); } if (deps.every(isPrimitive)) { console .warn( '`useDeepCompareEffect` should not be used with dependencies that are all primitive values. Use React.useEffect instead.' ); } } useCustomCompareEffect(effect, deps, isDeepEqual); }; export default useDeepCompareEffect;
使用浅比较effect。
语法:1 useShallowCompareEffect(effect: () => void | (() => void | undefined), deps: any[]);
参数:
effect:{Function},副作用回调
deps:{Array},依赖列表,等同于useEffect的第二个参数,可选;
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import {useCounter, useShallowCompareEffect} from 'react-use' ;const Demo = () => { const [count, {inc : inc}] = useCounter(0 ); const options = { step : 2 }; useShallowCompareEffect(() => { inc(options.step) }, [options]); return ( <div> <p>useShallowCompareEffect: {count}</p> </ div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import { DependencyList, EffectCallback } from 'react' ;import { equal as isShallowEqual } from 'fast-shallow-equal' ;import useCustomCompareEffect from './useCustomCompareEffect' ;const isPrimitive = (val: any ) => val !== Object (val);const shallowEqualDepsList = (prevDeps: DependencyList, nextDeps: DependencyList ) => prevDeps.every((dep, index ) => isShallowEqual(dep, nextDeps[index])); const useShallowCompareEffect = (effect: EffectCallback, deps: DependencyList ) => { if (process.env.NODE_ENV !== 'production' ) { if (!(deps instanceof Array ) || !deps.length) { console .warn('`useShallowCompareEffect` should not be used with no dependencies. Use React.useEffect instead.' ); } if (deps.every(isPrimitive)) { console .warn( '`useShallowCompareEffect` should not be used with dependencies that are all primitive values. Use React.useEffect instead.' ); } } useCustomCompareEffect(effect, deps, shallowEqualDepsList); }; export default useShallowCompareEffect;
控制requestAnimationFrame。
语法:1 const elapsed = useRaf(ms?: number, delay?: number): number;
参数:
ms:{Number},动画总时间,毫秒。默认是1e12,即1e9秒;
delay:{Number},延迟时间,毫秒。可选;
返回:
使用:1 2 3 4 5 6 7 8 9 10 11 import {useRaf} from 'react-use' ;const Demo = () => { const elapsed = useRaf(5000 , 1000 ); return ( <div> Elapsed: {elapsed} </div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import { useState } from 'react' ;import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect' ;const useRaf = (ms: number = 1e12 , delay : number = 0 ): number => { const [elapsed, set ] = useState<number>(0); useIsomorphicLayoutEffect(() => { let raf; let timerStop; let start; const onFrame = () => { const time = Math .min(1 , (Date .now() - start) / ms); set (time); loop(); }; const loop = () => { raf = requestAnimationFrame(onFrame); }; const onStart = () => { timerStop = setTimeout(() => { cancelAnimationFrame(raf); set (1); }, ms); start = Date.now(); loop(); }; const timerDelay = setTimeout(onStart, delay); return () => { clearTimeout(timerStop); clearTimeout(timerDelay); cancelAnimationFrame(raf); }; }, [ms, delay]); return elapsed; }; export default useRaf;
比较简单,通过requestAnimationFrame和setTimeout来控制整个运作。
setInterval的控制钩子。为何要单独抽离成一个钩子,可以见Dan Abramov’s article on overreacted.io ,写得非常好了。
语法:1 useInterval(callback, delay?: number)
参数:
callback:{Function},回调函数;
delay:{Number},定时器间隔,默认为0。
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import * as React from 'react' ;import {useInterval} from 'react-use' ;const Demo = () => { const [count, setCount] = React.useState(0 ); const [delay, setDelay] = React.useState(1000 ); const [isRunning, toggleIsRunning] = useBoolean(true ); useInterval( () => { setCount(count + 1 ); }, isRunning ? delay : null ); return ( <div> <div> delay: <input value ={delay} onChange ={event => setDelay(Number(event.target.value))} /> </div> <h1>count: {count}</ h1> <div> <button onClick={toggleIsRunning}>{isRunning ? 'stop' : 'start' }</button> </ div> </div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { useEffect, useRef } from 'react' ;const useInterval = (callback: Function , delay?: number | null ) => { const savedCallback = useRef<Function >(( ) => {}); useEffect(() => { savedCallback.current = callback; }); useEffect(() => { if (delay !== null ) { const interval = setInterval(() => savedCallback.current(), delay || 0 ); return () => clearInterval(interval); } return undefined ; }, [delay]); }; export default useInterval;
ref作为动画id避免函数组件的作用域问题。
同样是setInterval的控制钩子,但是所有的effect会在同样的延迟进行。
语法:1 useHarmonicIntervalFn(fn, delay?: number)
参数:
fn:{Function},回调函数;
delay:{Number},定时器间隔,默认为0。
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import * as React from 'react' ;import {useHarmonicIntervalFn} from 'react-use' ;const Demo = () => { const [count, setCount] = React.useState(0 ); const [delay, setDelay] = React.useState(1000 ); const [isRunning, toggleIsRunning] = useBoolean(true ); useHarmonicIntervalFn( () => { setCount(count + 1 ); }, isRunning ? delay : null ); return ( <div> <div> delay: <input value ={delay} onChange ={event => setDelay(Number(event.target.value))} /> </div> <h1>count: {count}</ h1> <div> <button onClick={toggleIsRunning}>{isRunning ? 'stop' : 'start' }</button> </ div> </div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { useEffect, useRef } from 'react' ;import { setHarmonicInterval, clearHarmonicInterval } from 'set-harmonic-interval' ;const useHarmonicIntervalFn = (fn: Function , delay: number | null = 0 ) => { const latestCallback = useRef<Function >(( ) => {}); useEffect(() => { latestCallback.current = fn; }); useEffect(() => { if (delay !== null ) { const interval = setHarmonicInterval(() => latestCallback.current(), delay); return () => clearHarmonicInterval(interval); } return undefined ; }, [delay]); }; export default useHarmonicIntervalFn;
核心的实现依赖于set-harmonic-interval 这个库。
弹簧力学的动力钩子。需要安装rebound
语法:1 const currentValue = useSpring(targetValue, tension, friction);
参数:
targetValue:{Number},目标值。可选,默认为0;
tension:{Number},张力。可选,默认为50;
friction:{Number},摩擦力。可选,默认为50。
返回:
currentValue:{Number},当前值
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import useSpring from 'react-use/lib/useSpring' ;const Demo = () => { const [target, setTarget] = useState(50 ); const value = useSpring(target); return ( <div> {value} <br /> <button onClick={() => setTarget(0 )}>Set 0 </button> <button onClick={() => setTarget(100)}>Set 100</ button> </div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import { useEffect, useMemo, useState } from 'react' ;import { Spring, SpringSystem } from 'rebound' ;const useSpring = (targetValue: number = 0 , tension: number = 50 , friction: number = 3 ) => { const [spring, setSpring] = useState<Spring | null >(null ); const [value, setValue] = useState<number>(targetValue); const listener = useMemo( () => ({ onSpringUpdate: currentSpring => { const newValue = currentSpring.getCurrentValue(); setValue(newValue); }, }), [] ); useEffect(() => { if (!spring) { const newSpring = new SpringSystem().createSpring(tension, friction); newSpring.setCurrentValue(targetValue); setSpring(newSpring); newSpring.addListener(listener); } return () => { if (spring) { spring.removeListener(listener); setSpring(null ); } }; }, [tension, friction, spring]); useEffect(() => { if (spring) { spring.setEndValue(targetValue); } }, [targetValue]); return value; }; export default useSpring;
对rebound的封装。
控制setTimeout。
语法:1 2 3 4 5 const [ isReady: () => boolean | null, cancel: () => void, reset: () => void, ] = useTimeoutFn(fn: Function, ms: number = 0);
参数:
fn:{Function},回调方法;
ms:{Number},延迟时间,毫秒。默认是0;
返回:
functions:{Function[]},控制方法集合
0:() => boolean | null
,当前是否已经完成;
1:() => void
,取消方法;
2:() => void
,重置方法
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import * as React from 'react' ;import { useTimeoutFn } from 'react-use' ;const Demo = () => { const [state, setState] = React.useState('Not called yet' ); function fn ( ) { setState(`called at ${Date .now()} ` ); } const [isReady, cancel, reset] = useTimeoutFn(fn, 5000 ); const cancelButtonClick = useCallback(() => { if (isReady() === false ) { cancel(); setState(`cancelled` ); } else { reset(); setState('Not called yet' ); } }, []); const readyState = isReady(); return ( <div> <div>{readyState !== null ? 'Function will be called in 5 seconds' : 'Timer cancelled' }</div> <button onClick={cancelButtonClick}> {readyState === false ? 'cancel' : 'restart'} timeout</ button> <br /> <div>Function state: {readyState === false ? 'Pending' : readyState ? 'Called' : 'Cancelled' }</div> <div>{state}</ div> </div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import { useCallback, useEffect, useRef } from 'react' ;export type UseTimeoutFnReturn = [() => boolean | null , () => void , () => void ];export default function useTimeoutFn (fn: Function, ms: number = 0 ): UseTimeoutFnReturn { const ready = useRef<boolean | null >(false ); const timeout = useRef<ReturnType<typeof setTimeout>>(); const callback = useRef(fn); const isReady = useCallback(() => ready.current, []); const set = useCallback(() => { ready.current = false ; timeout.current && clearTimeout(timeout.current); timeout.current = setTimeout(() => { ready.current = true ; callback.current(); }, ms); }, [ms]); const clear = useCallback(() => { ready.current = null ; timeout.current && clearTimeout(timeout.current); }, []); useEffect(() => { callback.current = fn; }, [fn]); useEffect(() => { set (); return clear; }, [ms]); return [isReady, clear, set ]; }
ref作为定时器id用来避免作用域问题。并且考虑了unmount时的定时器清除。
控制setTimeout。
语法:1 2 3 4 5 const [ isReady: () => boolean | null, cancel: () => void, reset: () => void, ] = useTimeout(ms: number = 0);
参数:
返回:
functions:{Function[]},控制方法集合
0:() => boolean | null
,当前是否已经完成;
1:() => void
,取消方法;
2:() => void
,重置方法
使用:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { useTimeout } from 'react-use' ;function TestComponent (props: { ms?: number } = {} ) { const ms = props.ms || 5000 ; const [isReady, cancel] = useTimeout(ms); return ( <div> { isReady() ? 'I\'m reloaded after timeout' : `I will be reloaded after ${ ms / 1000 }s` } { isReady() === false ? <button onClick={ cancel }>Cancel</ button> : '' } </div> ); } const Demo = () => { return ( <div> <TestComponent /> <TestComponent ms={ 10000 } /> </div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 import useTimeoutFn from './useTimeoutFn' ;import useUpdate from './useUpdate' ;export type UseTimeoutReturn = [() => boolean | null , () => void , () => void ];export default function useTimeout (ms: number = 0 ): UseTimeoutReturn { const update = useUpdate(); return useTimeoutFn(update, ms); }
基于useTimeoutFn,实现组件延迟更新render。
动画过渡钩子。
语法:1 const current = useTween(easing?: string, ms?: number, delay?: number): number
参数:
easing:{String},动画效果,字可见字段集合 。默认为"inCirc"
;
ms:{Number},动画总时间,毫秒。默认是200;
delay:{Number},延迟时间,毫秒。默认是0;
返回:
使用:1 2 3 4 5 6 7 8 9 10 11 import {useTween} from 'react-use' ;const Demo = () => { const t = useTween(); return ( <div> Tween: {t} </div> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import { easing } from 'ts-easing' ;import useRaf from './useRaf' ;export type Easing = (t: number ) => number;const useTween = (easingName: string = 'inCirc' , ms : number = 200 , delay : number = 0 ): number => { const fn: Easing = easing[easingName]; const t = useRaf(ms, delay); if (process.env.NODE_ENV !== 'production' ) { if (typeof fn !== 'function' ) { console .error( 'useTween() expected "easingName" property to be a valid easing function name, like:' + '"' + Object .keys(easing).join('", "' ) + '".' ); console .trace(); return 0 ; } } return fn(t); }; export default useTween;
依赖于ts-easing 动画库,动画控制核心依赖于requestAnimationFrame。
重新渲染组件。
语法:1 const update = useUpdate();
返回:
使用:1 2 3 4 5 6 7 8 9 10 11 import {useUpdate} from 'react-use' ;const Demo = () => { const update = useUpdate(); return ( <> <div>Time: {Date .now()}</div> <button onClick={update}>Update</ button> </> ); };
源码及解析 1 2 3 4 5 6 7 8 9 10 11 12 import { useReducer } from 'react' ;const updateReducer = (num: number): number => (num + 1 ) % 1 _000_000;const useUpdate = () => { const [, update] = useReducer(updateReducer, 0 ); return update as () => void ; }; export default useUpdate;
依赖于useReducer,以计数的形式来触发组件的更新;
1_000_000
其实就是1000000
;
Author
My name is Micheal Wayne and this is my blog.
I am a front-end software engineer.
Contact: michealwayne@163.com