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 { 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 { } }, [key, setState] ); const remove = useCallback (() => { try { localStorage .removeItem (key); setState (undefined ); } catch { } }, [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 { return initialValue; } }); useEffect (() => { try { const serializedState = raw ? String (state) : JSON .stringify (state); sessionStorage .setItem (key, serializedState); } catch { } }); 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