在React Conf 2018 宣布React Hooks后,我第一时间开始尝试使用React Hooks,现在新项目基本不写Class组件了。对我来说,它确实让我的开发效率提高了很多,改变了已有的组件开发思维和模式.
我在React组件设计实践总结04 - 组件的思维 中已经总结过React Hooks的意义,以及一些应用场景 。
那这篇文章就完全是介绍React Hooks的应用实例 ,列举了我使用React Hooks的一些实践。 希望通过这些案例,可以帮助你快速熟练,并迁移到React Hooks开发模式.
文章篇幅很长,建议收藏不看, 至少看看目录吧
把之前文章整理的React Hooks应用场景
总结拿过来, 本文基本按照这个范围进行组织 :
如果你想要了解React Hooks的原理可以阅读这些文章 :
目录索引
1. 组件状态 React提供了一个很基本的组件状态设置Hook:
const [state, setState] = useState(initialState);
useState 返回一个state,以及更新state的函数 . setState可以接受一个新的值,会触发组件重新渲染.
React会确保setState函数是稳定的,不会在组件重新渲染时改变 。下面的useReducer的dispatch函数、useRef的current属性也一样。这就意味着setState、dispatch、ref.current, 可以安全地在useEffect、useMemo、 useCallback中引用
1-1 useSetState 模拟传统的setState useState和Class组件的setState不太一样.
Class组件的state属性一般是一个对象,调用setState时,会浅拷贝到state属性, 并触发更新, 比如:
class MyComp extends React .Component { state = { name: '_sx_' , age: 10 } handleIncrementAge = () => { this .setState({age : this .state.age + 1 }) } }
而useState会直接覆盖state值 。为了实现和setState一样的效果, 可以这样子做:
const initialState = {name: 'sx' , age: 10 }const MyComp: FC = props => { const [state, setState] = useState(initialState) const handleIncrementAge = useCallback(() => { setState((prevState ) => ({...preState, age: prevState.age + 1 }) ) }, []) }
Ok,现在把它封装成通用的hooks,在其他组件中复用。这时候就体现出来Hooks强大的逻辑抽象能力:Hooks 旨在让组件的内部逻辑组织成可复用的更小单元,这些单元各自维护一部分组件‘状态和逻辑’
看看我们的useSetState
, 我会使用Typescript进行代码编写:
function useSetState <S extends object >( initalState: S | (( ) => S ),): [S, (state: Partial<S> | ((state: S) => Partial<S>)) => void] { const [_state, _setState] = useState<S>(initalState) const setState = useCallback((state: Partial<S> | ((state: S ) => Partial<S>)) => { _setState((prev: S ) => { let nextState = state if (typeof state === 'function' ) { nextState = state(prev) } return { ...prev, ...nextState } }) }, []) return [_state, setState] } export default function UseSetState ( ) { const [state, setState] = useSetState<{ name : string; age: number }>({ name : 'sx' , age : 1 }) const incrementAge = () => { setState(prev => ({ age : prev.age + 1 })) } return ( <div onClick={incrementAge}> {state.name}: {state.age} </div> ) }
hooks命名以use
为前缀
⤴️回到顶部
1-2 useReducer Redux风格状态管理 如果组件状态比较复杂,推荐使用useReducer来管理状态。如果你熟悉Redux,会很习惯这种方式。
const initialState = {count : 0 };function reducer (state, action ) { switch (action.type) { case 'increment' : return {count : state.count + 1 }; case 'decrement' : return {count : state.count - 1 }; default : throw new Error (); } } function Counter ( ) { const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({type : 'increment' })}>+</button > <button onClick={() => dispatch({type : 'decrement' })}>-</button > </> ); }
了解更多reducer的思想可以参考Redux文档
⤴️回到顶部
1-3 useForceUpdate 强制重新渲染 Class组件可以通过forceUpdate
实例方法来触发强制重新渲染。使用useState也可以模拟相同的效果:
export default function useForceUpdate ( ) { const [, setValue] = useState(0 ) return useCallback(() => { setValue(val => (val + 1 ) % (Number .MAX_SAFE_INTEGER - 1 )) }, []) } function ForceUpdate ( ) { const forceUpdate = useForceUpdate() useEffect(() => { somethingChange(forceUpdate) }, []) }
⤴️回到顶部
1-4 useStorage 简化localStorage存取 通过自定义Hooks,可以将状态代理到其他数据源,比如localStorage。 下面案例展示如果使用Hooks封装和简化localStorage的存取:
import { useState, useCallback, Dispatch, SetStateAction } from 'react' export default function useStorage <T >( key: string , defaultValue?: T | (() => T), keepOnWindowClosed: boolean = true , ): [T | undefined , Dispatch <SetStateAction <T >>, ( ) => void ] { const storage = keepOnWindowClosed ? localStorage : sessionStorage const getStorageValue = () => { try { const storageValue = storage.getItem(key) if (storageValue != null ) { return JSON .parse(storageValue) } else if (defaultValue) { const value = typeof defaultValue === 'function' ? (defaultValue as ( ) => T )() : defaultValue storage .setItem (key, JSON .stringify(value ) ) return value } } catch (err ) { console .warn (`useStorage 无法获取${key}: `, err ) } return undefined } const [value , setValue ] = useState <T | undefined >(getStorageValue ) // 更新组件状态并保存到Storage const save = useCallback <Dispatch <SetStateAction <T >>>(value => { setValue(prev => { const finalValue = typeof value === 'function ' ? (value as (prev: T | undefined ) => T )(prev ) : value storage.setItem(key, JSON .stringify(finalValue ) ) return finalValue } ) }, [] ) // 移除状态 const clear = useCallback (( ) => { storage.removeItem(key ) setValue(undefined ) }, [] ) return [value , save , clear ] } // -------- // EXAMPLE // -------- function Demo () { // 保存登录状态 const [use , setUser , clearUser ] = useStorage ('user' ) const handleLogin = (user ) => { setUser(user) } const handleLogout = () => { clearUser() } }
⤴️回到顶部
1-5 useRefState 引用state的最新值
上图是今年六月份VueConf ,尤雨溪的Slide截图,他对比了Vue最新的FunctionBase API 和React Hook. 它指出React Hooks有很多问题 :
每个Hooks在组件每次渲染时都执行。也就是说每次渲染都要重新创建闭包和对象
需要理解闭包变量
内容回调/对象会导致纯组件props比对失效, 导致组件永远更新
闭包变量问题是你掌握React Hooks过程中的重要一关。闭包问题是指什么呢?举个简单的例子, Counter:
function Counter ( ) { const [count, setCount] = useState(0 ) const handleIncr = () => { setCount(count + 1 ) } return (<div > {count}: <ComplexButton onClick ={handleIncr} > increment</ComplexButton > </div > ) }
假设ComplexButton是一个非常复杂的组件,每一次点击它,我们会递增count,从而触发组将重新渲染。因为Counter每次渲染都会重新生成handleIncr,所以也会导致ComplexButton重新渲染,不管ComplexButton使用了PureComponent
还是使用React.memo
包装 。
为了解决这个问题,React也提供了一个useCallback
Hook, 用来‘缓存’函数, 保持回调的不变性 . 比如我们可以这样使用:
function Counter ( ) { const [count, setCount] = useState(0 ) const handleIncr = useCallback(() => { setCount(count + 1 ) }, []) return (<div > {count}: <ComplexButton onClick ={handleIncr} > increment</ComplexButton > </div > ) }
上面的代码是有bug的,不过怎么点击,count会一直显示为1!
再仔细阅读useCallback的文档,useCallback支持第二个参数,当这些值变动时更新缓存的函数, useCallback的内部逻辑大概是这样的 :
let memoFn, memoArgsfunction useCallback (fn, args ) { if (!isEqual(memoArgs, args)) { memoArgs = args return (memoFn = fn) } return memoFn }
Ok, 现在理解一下为什么会一直显示1?
首次渲染时缓存了闭包,这时候闭包捕获的count值是0。在后续的重新渲染中,因为useCallback第二个参数指定的值没有变动,handleIncr闭包会永远被缓存。这就解释了为什么每次点击,count只能为1.
解决办法也很简单,让我们在count变动时,让useCallback更新缓存函数:
function Counter ( ) { const [count, setCount] = useState(0 ) const handleIncr = useCallback(() => { setCount(count + 1 ) }, [count]) return (<div>{count}: <ComplexButton onClick={handleIncr}>increment</ComplexButton></ div>) }
如果useCallback依赖很多值,你的代码可能是这样的:useCallback(fn, [a, b, c, d, e])
. 反正我是无法接受这种代码的,很容易遗漏, 而且可维护性很差,尽管通过ESLint插件 可以检查这些问题**。
其实通过useRef
Hook,可以让我们像Class组件一样保存一些‘实例变量’, React会保证useRef返回值的稳定性,我们可以在组件任何地方安全地引用ref。
基于这个原理,我们尝试封装一个useRefState
, 它在useState的基础上扩展了一个返回值,用于获取state的最新值:
import { useState, useRef, useCallback, Dispatch, SetStateAction, MutableRefObject } from 'react' function useRefState (initialState ) { const ins = useRef() const [state, setState] = useState(() => { const value = typeof initialState === 'function' ? initialState() : initialState ins.current = value return value }) const setValue = useCallback(value => { if (typeof value === 'function' ) { setState(prevState => { const finalValue = value(prevState) ins.current = finalValue return finalValue }) } else { ins.current = value setState(value) } }, []) return [state, setValue, ins] }
使用示例:
function Counter ( ) { const [count, setCount, countRef] = useRefState(0 ) const handleIncr = useCallback(() => { setCount(countRef.current + 1 ) }, []) useEffect(() => { return () => { saveCount(countRef.current) } }, []) return (<div>{count}: <ComplexButton onClick={handleIncr}>increment</ComplexButton></ div>) }
useEffect
、useMemo
和useCallback
一样存在闭包变量问题,它们和useCallback一个支持指定第二个参数,当这个参数变化时执行副作用。
⤴️回到顶部
1-5-1 每次重新渲染都创建闭包会影响效率吗? 函数组件和Class组件不一样的是,函数组件将所有状态和逻辑都放到一个函数中, 每一次重新渲染会重复创建大量的闭包、对象。而传统的Class组件的render函数则要简洁很多,一般只放置JSX渲染逻辑。相比大家都跟我一样,会怀疑函数组件的性能问题
我们看看官方是怎么回应的:
我在SegmentFault的react function组件与class组件性能问题 也进行了详细的回答, 结论是:
目前而言,实现同样的功能,类组件和函数组件的效率是不相上下的。但是函数组件是未来,而且还有优化空间,React团队会继续优化它。而类组件会逐渐退出历史
为了提高函数组件的性能,可以在这些地方做一些优化 :
能否将函数提取为静态的
const goback = () => { history.go(-1 ) } function Demo ( ) { return <button onClick ={goback} > back</button > } const returnEmptyObject = () => Object .create(null )const returnEmptyArray = () => []function Demo ( ) { const [state, setState] = useState(returnEmptyObject) const [arr, setArr] = useState(returnEmptyArray) }
简化组件的复杂度,动静分离
再拆分更细粒度的组件,这些组件使用React.memo缓存
⤴️回到顶部
1-6 useRefProps 引用最新的Props 现实项目中也有很多这种场景: 我们想在组件的任何地方获取最新的props值 ,这个同样可以通过useRef来实现:
export default function useRefProps <T >(props: T ) { const ref = useRef<T>(props) ref.current = props return ref } function MyButton (props ) { const propsRef = useRefProps(props) const handleClick = useCallback(() => { const { onClick } = propsRef.current if (onClick) { onClick() } }, []) return <ComplexButton onClick ={handleClick} > </ComplexButton > }
⤴️回到顶部
1-7 useInstance ‘实例’变量存取 function isFunction <T >(initial?: T | (() => T) ): initial is ( ) => T { return typeof initial === 'function' } function useInstance <T extends {}>(initial?: T | (( ) => T ) ) { const instance = useRef <T >() // 初始化 if (instance.current == null ) { if (initial ) { instance .current = isFunction (initial ) ? initial () : initial } else { instance .current = {} as T } } return instance .current } // --------- // EXAMPLE // --------- function Demo () { const inst = useInstance ({ count: 1 } ) const update = useForceUpdate () useEffect (( ) => { const timer = setInterval(( ) => { inst.count++ }, 1000 ) return ( ) => clearInterval(timer ) }, [] ) return ( <div> count: {inst.count} <button onClick={update}>刷新</button> </div> )}
注意不要滥用
⤴️回到顶部
1-9 usePrevious 获取上一次渲染的值 在Class组件中,我们经常会在shouldComponentUpdate
或componentDidUpdate
这类生命周期方法中对props或state进行比对,来决定做某些事情,例如重新发起请求、监听事件等等.
Hooks中我们可以使用useEffect或useMemo来响应状态变化,进行状态或副作用衍生. 所以上述比对的场景在Hooks中很少见。但也不是不可能,React官方案例中就有一个usePrevious
:
function usePrevious (value ) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; } const calculation = count * 100 ;const prevCalculation = usePrevious(calculation);
⤴️回到顶部
1-10 useImmer 简化不可变数据操作 这个案例来源于use-immer , 结合immer.js 和Hooks来简化不可变数据操作, 看看代码示例:
const [person, updatePerson] = useImmer({ name: "Michel" , age: 33 }); function updateName (name ) { updatePerson(draft => { draft.name = name; }); } function becomeOlder ( ) { updatePerson(draft => { draft.age++; }); }
实现也非常简单:
export function useImmer (initialValue ) { const [val, updateValue] = useState(initialValue); return [ val, useCallback(updater => { updateValue(produce(updater)); }, []) ]; }
简洁的Hooks配合简洁的Immer,简直完美
⤴️回到顶部
1-11 封装’工具Hooks’简化State的操作 Hooks只是普通函数,所以可以灵活地自定义。下面举一些例子,利用自定义Hooks来简化常见的数据操作场景
1-11-1 useToggle 开关 实现boolean值切换
function useToggle (initialValue?: boolean ) { const [value, setValue] = useState(!!initialValue) const toggler = useCallback(() => setValue(value => !value), []) return [value, toggler] } function Demo ( ) { const [enable, toggleEnable] = useToggle() return <Switch value={enable} onClick={toggleEnable}></Switch> }
⤴️回到顶部
1-11-2 useArray 简化数组状态操作 function useArray <T >(initial?: T[] | (() => T[]), idKey: string = 'id' ) { const [value, setValue] = useState(initial || []) return { value, setValue, push: useCallback(a => setValue(v => [...v, a]), []), clear: useCallback(() => setValue(() => []), []), removeById: useCallback(id => setValue(arr => arr.filter(v => v && v[idKey] !== id)), []), removeIndex: useCallback( index => setValue(v => { v.splice(index, 1 ) return v }), [], ), } } function Demo ( ) { const {value, push, removeById} = useArray<{id: number , name: string }>() const handleAdd = useCallback(() => { push({id: Math .random(), name: getName()}) }, []) return (<div> <div>{value.map(i => <span key={i.id} onClick={() => removeById(i.id)}>{i.name}</span>)}</ div> <button onClick={handleAdd}>add</button> </ div>)}
限于篇幅,其他数据结构, 例如Set、Map, 就不展开介绍了,读者可以自己发挥想象力.
⤴️回到顶部
2. 模拟生命周期函数 组件生命周期相关的操作依赖于useEffect
Hook. React在函数组件中刻意淡化了组件生命周期的概念,而更关注‘数据的响应’ .
useEffect
名称意图非常明显,就是专门用来管理组件的副作用 。和useCallback一样,useEffect支持传递第二个参数,告知React在这些值发生变动时才执行父作用. 原理大概如下:
let memoCallback = {fn : undefined , disposer : undefined }let memoArgsfunction useEffect (fn, args ) { if (args == null || !isEqual(memoArgs, args)) { memoArgs = args memoCallback.fn = fn pushIntoEffectQueue(memoCallback) } } function queueExecute (callback ) { if (callback.disposer) { callback.disposer() } callback.disposer = callback.fn() }
关于useEffect官网有详尽的描述 ; Dan Abramov也写了一篇useEffect 完整指南 , 推荐👍。
⤴️回到顶部
2-1 useOnMount 模拟componentDidMount export default function useOnMount (fn: Function ) { useEffect(() => { fn() }, []) } function Demo ( ) { useOnMount(async () => { try { await loadList() } catch { } }) }
如果需要在挂载/状态更新时请求一些资源、并且需要在卸载时释放这些资源,还是推荐使用useEffect,因为这些逻辑最好放在一起, 方便维护和理解 :
useEffect(() => { const subscription = props.source.subscribe(); return () => { subscription.unsubscribe(); }; }, []);
⤴️回到顶部
2-2 useOnUnmount 模拟componentWillUnmount export default function useOnUnmount (fn: Function ) { useEffect(() => { return () => { fn() } }, []) }
⤴️回到顶部
2-3 useOnUpdate 模拟componentDidUpdate function useOnUpdate (fn: () => void , dep?: any [] ) { const ref = useRef({ fn, mounted: false }) ref.current.fn = fn useEffect(() => { if (!ref.current.mounted) { ref.current.mounted = true } else { ref.current.fn() } }, dep) } function Demo (props ) { useOnUpdate(() => { dosomethingwith(props.a) }, [props.a]) return <div>...</div> }
其他生命周期函数的模拟:
shouldComponentUpdate
- React.memo包裹组件
componentDidCatch
- 暂不支持
⤴️回到顶部
3. 事件处理 3-1 useChange 简化onChange表单双向绑定 表单值的双向绑定在项目中非常常见,通常我们的代码是这样的:
function Demo ( ) { const [value, setValue] = useState('' ) const handleChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>(evt => { setValue(evt.target.value) }, []) return <input value={value} onChange={handleChange} /> }
如果需要维护多个表单,这种代码就会变得难以接受。幸好有Hooks,我们可以简化这些代码:
function useChange <S >(initial?: S | (() => S) ) { const [value, setValue] = useState<S | undefined >(initial) const onChange = useCallback(e => setValue(e.target.value), []) return { value, setValue, onChange, bindEvent: { onChange, value, }, bind: { onChange: setValue, value, }, } } function Demo ( ) { const userName = useChange('' ) const password = useChange('' ) return ( <div> <input {...userName.bindEvent} /> <input type ="password" {...password.bindEvent} /> </div> ) }
⤴️回到顶部
3-2 useBind 绑定回调参数 绑定一些回调参数,并利用useMemo给下级传递一个缓存的回调, 避免重新渲染:
function useBind (fn?: (...args: any []) => any , ...args: any [] ): (...args: any [] ) => any { return useMemo(() => {fn && fn.bind(null , ...args)}, args) } function Demo (props ) { const {id, onClick} = props const handleClick = useBind(onClick, id) return <ComplexComponent onClick={handleClick}></ComplexComponent> } / / 等价于 function Demo(props) { const {id, onClick} = props const handleClick = useCallback(() => onClick(id), [id]) return <ComplexComponent onClick={handleClick}></ ComplexComponent>}
⤴️回到顶部
3-3 自定义事件封装 Hooks也可以用于封装一些高级事件或者简化事件的处理,比如拖拽、手势、鼠标Active/Hover等等;
3-3-1 useActive 举个简单的例子, useActive, 在鼠标按下时设置状态为true,鼠标释放时恢复为false:
function useActive (refEl: React.RefObject<HTMLElement> ) { const [value, setValue] = useState(false ) useEffect(() => { const handleMouseDown = () => { setValue(true ) } const handleMouseUp = () => { setValue(false ) } if (refEl && refEl.current) { refEl.current.addEventListener('mousedown' , handleMouseDown) refEl.current.addEventListener('mouseup' , handleMouseUp) } return () => { if (refEl && refEl.current) { refEl.current.removeEventListener('mousedown' , handleMouseDown) refEl.current.removeEventListener('mouseup' , handleMouseUp) } } }, []) return value } function Demo ( ) { const elRef = useRef(null ) const active = useActive(inputRef) return (<div ref={elRef}>{active ? "Active" : "Nop" }</div>) }
⤴️回到顶部
3-3-2 useTouch 手势事件封装 更复杂的自定义事件, 例如手势。限于篇幅就不列举它们的实现代码,我们可以看看它们的Demo:
function Demo ( ) { const {ref} = useTouch({ onTap: () => { }, onLongTap: () => { }, onRotate: () => {} }) return (<div className="box" ref={ref}></div>) }
useTouch的实现可以参考useTouch.ts
⤴️回到顶部
3-3-3 useDraggable 拖拽事件封装 拖拽也是一个典型的自定义事件, 下面这个例子来源于这里
function useDraggable (ref: React.RefObject<HTMLElement> ) { const [{ dx, dy }, setOffset] = useState({ dx: 0 , dy: 0 }) useEffect(() => { if (ref.current == null ) { throw new Error (`[useDraggable] ref未注册到组件中` ) } const el = ref.current const handleMouseDown = (event: MouseEvent ) => { const startX = event.pageX - dx const startY = event.pageY - dy const handleMouseMove = (event: MouseEvent ) => { const newDx = event.pageX - startX const newDy = event.pageY - startY setOffset({ dx: newDx, dy: newDy }) } document .addEventListener('mousemove' , handleMouseMove) document .addEventListener( 'mouseup' , () => { document .removeEventListener('mousemove' , handleMouseMove) }, { once: true }, ) } el.addEventListener('mousedown' , handleMouseDown) return () => { el.removeEventListener('mousedown' , handleMouseDown) } }, [dx, dy]) useEffect(() => { if (ref.current) { ref.current.style.transform = `translate3d(${dx} px, ${dy} px, 0)` } }, [dx, dy]) } function Demo ( ) { const el = useRef(); useDraggable(el); return <div className="box" ref={el} /> }
可运行例子
⤴️回到顶部
3-3-4 react-events 面向未来的高级事件封装 我在<谈谈React事件机制和未来(react-events)> 介绍了React-Events
这个实验性 的API。当这个API成熟后,我们可以基于它来实现更优雅的高级事件的封装:
import { PressResponder, usePressListener } from 'react-events/press' ;const Button = (props ) => ( const listener = usePressListener({ onPressStart, onPress, onPressEnd, }) return ( <div listeners={listener}> {subtrees} </div> ); );
⤴️回到顶部
3-4 useSubscription 通用事件源订阅 React官方维护了一个use-subscription 包,支持使用Hooks的形式来监听事件源. 事件源可以是DOM事件、RxJS的Observable等等.
先来看看使用示例:
function Demo ( ) { const subscription = useMemo( () => ({ getCurrentValue: () => behaviorSubject.getValue(), subscribe: callback => { const subscription = behaviorSubject.subscribe(callback); return () => subscription.unsubscribe(); } }), [behaviorSubject] ); const value = useSubscription(subscription); return <div > {value}</div > }
现在来看看实现:
export function useSubscription <T >({ getCurrentValue, subscribe, }: { getCurrentValue?: () => T subscribe: (callback: Function ) => () => void } ): T { const [state, setState] = useState(() => ({ getCurrentValue, subscribe, value: getCurrentValue() })) let valueToReturn = state.value if (state.getCurrentValue !== getCurrentValue || state.subscribe !== subscribe) { valueToReturn = getCurrentValue() setState({ getCurrentValue, subscribe, value: valueToReturn }) } useEffect(() => { let didUnsubscribe = false const checkForUpdates = () => { if (didUnsubscribe) { return } setState(prevState => { if (prevState.getCurrentValue !== getCurrentValue || prevState.subscribe !== subscribe) { return prevState } const value = getCurrentValue() if (prevState.value === value) { return prevState } return { ...prevState, value } }) } const unsubscribe = subscribe(checkForUpdates) checkForUpdates() return () => { didUnsubscribe = true unsubscribe() } }, [getCurrentValue, subscribe]) return valueToReturn }
实现也不复杂,甚至可以说有点啰嗦.
⤴️回到顶部
3-5 useObservable Hooks和RxJS优雅的结合(rxjs-hooks) 如果要配合RxJS使用,LeetCode团队封装了一个rxjs-hooks 库,用起来则要优雅很多, 非常推荐:
function App ( ) { const value = useObservable(() => interval(500 ).pipe(map((val ) => val * 3 ))); return ( <div className="App" > <h1>Incremental number : {value}</h1> </ div> ); }
⤴️回到顶部
3-6 useEventEmitter 对接eventEmitter 我在React组件设计实践总结04 - 组件的思维 这篇文章里面提过:自定义 hook 和函数组件的代码结构基本一致, 所以有时候hooks 写着写着原来越像组件, 组件写着写着越像 hooks. 我觉得可以认为组件就是一种特殊的 hook, 只不过它输出 Virtual DOM
Hooks跟组件一样,是一个逻辑和状态的聚合单元。可以维护自己的状态、有自己的’生命周期’.
useEventEmitter
就是一个典型的例子,可以独立地维护和释放自己的资源:
const function ReturnObject = ( ) => ({} )const function ReturnArray = ( ) => []export function useEventEmitter (emmiter: EventEmitter ) { const disposers = useRef <Function []>([] ) const listeners = useRef < { [name : string ]: Function }>({} ) const on = useCallback (<P>(name: string , cb: (data: P) => void ) => { if (!(name in listeners.current)) { const call = (...args: any []) => { const fn = listeners.current[name] if (fn) { fn(...args) } } emmiter.on(name, call) disposers.current.push(() => { emmiter.off(name, call) }) } listeners.current[name] = cb }, [] ) useEffect (() => { return () => { disposers.current.forEach(i => i()) } }, [] ) return { on, emit: emmiter.emit, } } function Demo ( ) { const { on, emit } = useEventEmitter(eventBus) on('someEvent' , () => { }) const handleClick = useCallback(() => { emit('anotherEvent' , someData) }, []) return (<div onClick={handleClick}>...</div>) }
更多脑洞:
⤴️回到顶部
4. Context的妙用 通过useContext
可以方便地引用Context。不过需要注意的是如果上级Context.Provider
的value变化,使用useContext的组件就会被强制重新渲染。
4-1 useTheme 主题配置 原本需要使用高阶组件注入或Context.Consumer获取的Context值,现在变得非常简洁:
withTheme(MyComponent) const MyComponentWithTheme = (props ) => { return (<ThemeContext.Consumer> {value => <MyComponent theme={value} {...props}></MyComponent>} </ ThemeContext.Consumer>)}
Hooks方式
import React, { useContext, FC } from 'react' const ThemeContext = React.createContext<object>({})export const ThemeProvider: FC<{ theme: object }> = props => { return <ThemeContext.Provider value={props.theme}>{props.children}</ThemeContext.Provider> } export function useTheme<T extends object>(): T { return useContext(ThemeContext) } / / --------- / / EXAMPLE / / --------- const theme = { primary: '#000', secondary: '#444', } function App() { return ( <ThemeProvider theme={theme}> <div>...</ div> </ThemeProvider> ) } const Button: FC = props => { const t = useTheme<typeof theme>() const style = { color: t.primary, } return <button style={style}>{props.children}</ button>}
⤴️回到顶部
4-2 unstated 简单状态管理器 Hooks + Context 也可以用于实现简单的状态管理。
我在React组件设计实践总结05 - 状态管理 就提到过unstated-next , 这个库只有主体代码十几行,利用了React本身的机制来实现状态管理 .
先来看看使用示例
import React, { useState } from "react" import { createContainer } from "unstated-next" function useCounter (initialState = 0 ) { let [count, setCount] = useState(initialState) let decrement = () => setCount(count - 1 ) let increment = () => setCount(count + 1 ) return { count, decrement, increment } } let Counter = createContainer(useCounter)function CounterDisplay ( ) { let counter = Counter.useContainer() return ( <div> <button onClick={counter.decrement}>-</button> <span>{counter.count}</ span> <button onClick={counter.increment}>+</button> </ div> ) }
看看它的源码:
export function createContainer (useHook ) { let Context = React.createContext(null ) function Provider (props ) { let value = useHook(props.initialState) return <Context.Provider value={value}>{props.children}</Context.Provider> } function useContainer() { / / 只是使用useContext let value = React.useContext(Context) if (value === null) { throw new Error("Component must be wrapped with <Container.Provider>") } return value } return { Provider, useContainer } } export function useContainer(container) { return container.useContainer() }
到这里,你会说,我靠,就这样? 这个库感觉啥事情都没干啊?
需要注意的是, Context不是万金油,它作为状态管理有一个比较致命的缺陷 ,我在浅谈React性能优化的方向 文章中也提到了这一点:它是可以穿透React.memo或者shouldComponentUpdate的比对的,也就是说,一旦 Context 的 Value 变动,所有依赖该 Context 的组件会全部 forceUpdate
所以如果你打算使用Context作为状态管理,一定要注意规避这一点. 它可能会导致组件频繁重新渲染.
其他状态管理方案:
⤴️回到顶部
4-3 useI18n 国际化 I18n是另一个Context的典型使用场景。react-intl 和react-i18next 都与时俱进,推出了自己的Hook API, 基本上原本使用高阶组件(HOC)实现的功能都可以用Hooks代替,让代码变得更加简洁 :
import React from 'react' ;import { useTranslation } from 'react-i18next' ;export function MyComponent ( ) { const { t, i18n } = useTranslation(); return <p > {t('my translated text')}</p > }
⤴️回到顶部
4-4 useRouter 简化路由状态的访问 React Hooks 推出已经接近一年,ReactRouter竟然还没有正式推出Hook API。不过它们也提上了计划 —— The Future of React Router and @reach/router ,5.X版本会推出Hook API. 我们暂时先看看一些代码示例:
function SomeComponent ( ) { const { userId } = useParams() } function usePageViews ( ) { const { location } = useLocation() useEffect(() => { ga('send' , 'pageview' , location.pathname) }, [location]) }
再等等吧!
⤴️回到顶部
react-hook-form 是Hooks+Form的典型案例,比较符合我理想中的表单管理方式:
import React from 'react' ;import useForm from 'react-hook-form' ;function App ( ) { const { register, handleSubmit, errors } = useForm(); const onSubmit = data => { console .log(data); }; return ( <form onSubmit={handleSubmit(onSubmit)}> <input name="firstname" ref={register} /> {} <input name="lastname" ref={register({ required : true })} /> {errors.lastname && 'Last name is required.' } <input name="age" ref={register({ pattern : /\d+/ })} /> {errors.age && 'Please enter number for age.' } <input type="submit" /> </form> ); }
⤴️回到顶部
5. 副作用封装 我们可以利用Hooks来封装或监听组件外部的副作用,将它们转换为组件的状态 。
5-1 useTimeout 超时修改状态 useTimeout由用户触发,在指定时间后恢复状态. 比如可以用于’短期禁用’按钮, 避免重复点击:
function Demo ( ) { const [disabled, start] = useTimeout(5000 ) const handleClick = () => { start() dosomething() } return <Button onClick={handleClick} disabled={disabled}>点我</Button> }
实现:
function useTimeout (ms: string ) { const [ready, setReady] = useState(false ) const timerRef = useRef<number >() const start = useCallback(() => { clearTimeout(timerRef.current) setReady(true ) timerRef.current = setTimeout(() => { setReady(false ) }, ms) }, [ms]) const stop = useCallback(() => { clearTimeout(timeRef.current) }, []) useOnUnmount(stop) return [ready, start, stop] }
⤴️回到顶部
5-2 useOnlineStatus 监听在线状态 副作用封装一个比较典型的案例就是监听主机的在线状态:
function getOnlineStatus ( ) { return typeof navigator.onLine === 'boolean' ? navigator.onLine : true } function useOnlineStatus ( ) { let [onlineStatus, setOnlineStatus] = useState(getOnlineStatus()) useEffect(() => { const online = () => setOnlineStatus(true ) const offline = () => setOnlineStatus(false ) window .addEventListener('online' , online) window .addEventListener('offline' , offline) return () => { window .removeEventListener('online' , online) window .removeEventListener('offline' , offline) } }, []) return onlineStatus } function Demo ( ) { let onlineStatus = useOnlineStatus(); return ( <div> <h1>网络状态: {onlineStatus ? "在线" : "离线" }</h1> </ div> ); }
还有很多案例, 这里就不一一列举,读者可以自己尝试去实现,比如:
useDeviceOrientation 监听设备方向
useGeolocation 监听GPS坐标变化
useScrollPosition 监听滚动位置
useMotion 监听设备运动
useMediaDevice 监听媒体设备
useDarkMode 夜间模式监听
useKeyBindings 监听快捷键
….
⤴️回到顶部
6. 副作用衍生 和副作用封装
相反,副作用衍生是指当组件状态变化时,衍生出其他副作用. 两者的方向是相反的 .
副作用衍生主要会用到useEffect,使用useEffect来响应状态的变化.
6-1 useTitle 设置文档title useTitle是最简单的,当给定的值变化时,更新document.title
function useTitle (t: string ) { useEffect(() => { document .title = t }, [t]) } function Demo (props ) { useTitle(props.isEdit ? '编辑' : '新增' ) }
⤴️回到顶部
6-2 useDebounce 再来个复杂一点的,useDebounce:当某些状态变化时,它会延迟执行某些操作:
function useDebounce (fn: () => void , args?: any [], ms: number = 100, skipMount?: boolean ) { const mounted = useRef(false ) useEffect(() => { if (skipMount && !mounted.current) { mounted.current = true return undefined } const timer = setTimeout(fn, ms) return () => { clearTimeout(timer) } }, args) } const returnEmptyArray = () => []function Demo ( ) { const [query, setQuery] = useState('' ) const [list, setList] = useState(returnEmptyArray) const handleSearch = async () => { setList(await fetchList(query)) } useDebounce(handleSearch, [query], 500 ) return (<div> <SearchBar value={query} onChange={setQuery} /> <Result list={list}></Result> </ div>)}
⤴️回到顶部
6-3 useThrottle 同理可以实现useThrottle, 下面的例子来源于react-use :
const useThrottleFn = <T>(fn: (...args: any [] ) => T, ms: number = 200, args: any [] ) => { const [state, setState] = useState<T>(null as any ); const timeout = useRef<any >(null ); const nextArgs = useRef(null ) as any ; const hasNextArgs = useRef(false ) as any ; useEffect(() => { if (!timeout.current) { setState(fn(...args)); const timeoutCallback = () => { if (hasNextArgs.current) { hasNextArgs.current = false ; setState(fn(...nextArgs.current)); timeout.current = setTimeout(timeoutCallback, ms); } else { timeout.current = null ; } }; timeout.current = setTimeout(timeoutCallback, ms); } else { nextArgs.current = args; hasNextArgs.current = true ; } }, args); useOnUnmount(() => { clearTimeout(timeout.current); }); return state; };
⤴️回到顶部
7. 简化业务逻辑 80%的程序员80%的时间在写业务代码 . 有了Hooks,React开发者如获至宝. 组件的代码可以变得很精简,且这些Hooks可以方便地在组件之间复用:
下面介绍,如何利用Hooks来简化业务代码
7-1 usePromise 封装异步请求 第一个例子,试试封装一下promise,简化简单页面异步请求的流程. 先来看看usePromise的使用示例,我理想中的usePromise应该长这样:
function Demo() { const list = usePromise(async (id: string) => { return fetchList(id) }) return (<div> {/* 触发请求 */} <button onClick={() => list.callIgnoreError('myId')}>Get List</button> {/* 错误信息展示和重试 */} {!!list.error && <ErrorMessage error={list.error} retry={list.retry}>加载失败:</ErrorMessage>} {/* 加载状态 */} <Loader loading={list.loading}> {/* 请求结果 */} <Result value={list.value}></Result> </Loader> </div>) }
usePromise是我用得比较多的一个Hooks,所以我把它完整的代码,包括Typescript注解都贴出来,供大家参考参考:
export interface Res<T, S> { loading: boolean error?: Error value?: S setValue: (v: S ) => void call: T callIgnoreError: T reset: () => void retry: () => void } export interface UsePromiseOptions { skipOnLoading?: boolean } function usePromise <T >(action: () => Promise <T>, option?: UsePromiseOptions ): Res <( ) => Promise <T >, T >function usePromise <T , A >(action: (arg0: A) => Promise <T>, option?: UsePromiseOptions ): Res <(arg0: A ) => Promise <T >, T >function usePromise <T , A , B >(action: (arg0: A, arg1: B) => Promise <T>, option?: UsePromiseOptions ): Res <(arg0: A, arg1: B ) => Promise <T >, T >function usePromise <T , A , B , C >( action: (arg0: A, arg1: B, arg2: C) => Promise <T>, option?: UsePromiseOptions ): Res <(arg0: A, arg1: B, arg2: C ) => Promise <T >, T >function usePromise <T , A , B , C , D >(action: (arg0: A, arg1: B, arg2: C, arg3: D) => Promise <T>, option?: UsePromiseOptions ): Res <(arg0: A, arg1: B, arg2: C, arg3: D ) => Promise <T >, T >function usePromise (action: (...args: any []) => Promise <any >, option?: UsePromiseOptions ): Res <(...args: any ) => Promise <any >, any >// 👆 上面是一堆Typescript 函数重载声明,可以跳过 /** * 接受一个action ,用于执行异步操作 */ function usePromise ( action: (...args: any []) => Promise <any >, option: UsePromiseOptions = { skipOnLoading: true }, ): Res <(...args: any ) => Promise <any >, any > { const actionRef = useRefProps (action ) const optionRef = useRefProps (option ) const [loading , setLoading , loadingRef ] = useRefState (false ) const taskIdRef = useRef <number >( ) const argsRef = useRef <any []>( ) const [value , setValue ] = useState ( ) const [error , setError , errorRef ] = useRefState <Error | undefined >( ) const caller = useCallback (async (...args: any []) => { argsRef.current = args if (loadingRef.current && optionRef.current.skipOnLoading) { return } const taskId = getUid() taskIdRef.current = taskId const shouldContinue = () => { if (taskId !== taskIdRef.current) { return false } return true } try { setLoading(true ) setError(undefined ) const res = await actionRef.current(...args) if (!shouldContinue()) return setValue(res) return res } catch (err) { if (shouldContinue()) { setError(err) } throw err } finally { if (shouldContinue()) { setLoading(false ) } } }, [] ) // 不抛出异常 const callIgnoreError = useCallback ( async (...args: any []) => { try { return await caller(...args) } catch { } }, [caller], ) const reset = useCallback (() => { setLoading(false ) setValue(undefined ) setError(undefined ) }, [] ) // 失败后重试 const retry = useCallback (() => { if (argsRef.current && errorRef.current) { return callIgnoreError(...argsRef.current) } throw new Error (`not call yet`) }, [] ) return { loading , error , call : caller , callIgnoreError , value , setValue , reset , retry , } }
⤴️回到顶部
7-2 usePromiseEffect 自动进行异步请求 很多时候,我们是在组件一挂载或者某些状态变化时自动进行一步请求的,我们在usePromise的基础上,结合useEffect来实现自动调用:
export default function usePromiseEffect <T >( action: (...args: any []) => Promise <T>, args?: any [], ) { const prom = usePromise(action) useEffect(() => { prom.callIgnoreError.apply(null , args) }, args) return prom } function Demo (props ) { const list = usePromiseEffect((id ) => fetchById(id), [id]) }
看到这里,应该惊叹Hooks的抽象能力了吧!😸
⤴️回到顶部
7-3 useInfiniteList 实现无限加载列表 这里例子在之前的文章中也提及过
export default function useInfiniteList <T >( fn: (params: { offset: number ; pageSize: number ; list: T[] }) => Promise <T[]>, pageSize: number = 20, ) { const [list, setList] = useState<T[]>(returnEmptyArray) const [hasMore, setHasMore, hasMoreRef] = useRefState(true ) const [empty, setEmpty] = useState(false ) const promise = usePromise(() => fn({ list, offset: list.length, pageSize })) const load = useCallback(async () => { if (!hasMoreRef.current) { return } const res = await promise.call() if (res.length < pageSize) { setHasMore(false ) } setList(l => { if (res.length === 0 && l.length === 0 ) { setEmpty(true ) } return [...l, ...res] }) }, []) const clean = useCallback(() => { setList([]) setHasMore(true ) setEmpty(false ) promise.reset() }, []) const refresh = useCallback(() => { clean() setTimeout(() => { load() }) }, []) return { list, hasMore, empty, loading: promise.loading, error: promise.error, load, refresh, } }
使用示例:
interface Item { id: number name: string } function App ( ) { const { load, list, hasMore, refresh } = useInfiniteList<Item>(async ({ offset, pageSize } ) => { const list = [] for (let i = offset; i < offset + pageSize; i++ ) { if (i === 200 ) { break } list.push({ id: i, name: `${i}-----` } ) } return list } ) useEffect (( ) => { load( ) }, [] ) return ( <div className="App"> <button onClick={refresh}>Refresh</button> {list.map(i => ( <div key={i.id}>{i.name}</div> ) )} {hasMore ? <button onClick={load}>Load more</button> : <div>No more</div>} </div> )}
⤴️回到顶部
7-4 usePoll 用hook实现轮询 下面使用Hooks实现一个定时轮询器
export interface UsePollOptions<T> { condition: (arg?: T, error?: Error ) => Promise <boolean > poller: () => Promise <T> onError?: (err: Error ) => void duration?: number args?: any [] immediately?: boolean } export default function usePoll <T = any >(options: UsePollOptions<T> ) { const [polling, setPolling, pollingRef] = useRefState(false ) const [error, setError] = useState<Error >() const state = useInstance<{ timer?: number ; unmounted?: boolean }>({}) const optionsRef = useRefProps(options) const poll = useCallback(async (immediate?: boolean ) => { if (state.unmounted || pollingRef.current) return setPolling(true ) state.timer = window .setTimeout( async () => { if (state.unmounted) return try { let res: T | undefined let error: Error | undefined setError(undefined ) try { res = await optionsRef.current.poller() } catch (err) { error = err setError(err) if (optionsRef.current.onError) { optionsRef.current.onError(err) } } if (!state.unmounted && (await optionsRef.current.condition(res, error))) { setTimeout(poll) } } finally { !state.unmounted && setPolling(false ) } }, immediate ? 0 : optionsRef.current.duration || 5000 , ) }, []) useOnUpdate( async () => { if (await optionsRef.current.condition()) poll(options.immediately) }, options.args || [], false , ) useOnUnmount(() => { state.unmounted = true clearTimeout(state.timer) }) return { polling, error } }
使用示例:
function Demo ( ) { const [query, setQuery] = useState(') const [result, setResult] = useState<Result>() usePoll({ poller: await() => { const res =await fetch(query) setResult(res) return res } condition: async () => { return query !== ' ' }, args: [query], }) // ... }
⤴️回到顶部
7-5 业务逻辑抽离 通过上面的案例可以看到, Hooks非常适合用于抽离重复的业务逻辑。
在React组件设计实践总结02 - 组件的组织 介绍了容器组件和展示组件分离,Hooks时代,我们可以自然地将逻辑都放置到Hooks中,实现逻辑和视图的分离 。
抽离的后业务逻辑可以复用于不同的’展示平台’, 例如 web 版和 native 版:
Login/ useLogin.ts // 将所有逻辑都抽取到Hooks中 index.web.tsx // 只保留视图 index.tsx
⤴️回到顶部
8. 开脑洞 一些奇奇怪怪的东西,不知道怎么分类。作者想象力非常丰富!
8-1 useScript: Hooks + Suspend = ❤️ 这个案例来源于the-platform , 使用script标签来加载外部脚本:
import {createResource} from 'react-cache' export const ScriptResource = createResource((src: string ) => { return new Promise ((resolve, reject ) => { const script = document .createElement('script' ); script.src = src; script.onload = () => resolve(script); script.onerror = reject; document .body.appendChild(script); }); }); function useScript (options: { src: string } ) { return ScriptResource.read(src); }
使用示例:
import { useScript } from 'the-platform' ;const Example = () => { useScript({ src: 'bundle.js' }); }; function App ( ) { return <Suspense fallback={'loading...' }><Example></Example></ Suspense> }
同理还可以实现
⤴️回到顶部
8-2 useModal 模态框数据流管理 我在React组件设计实践总结04 - 组件的思维 也举到一个使用Hooks + Context
来巧妙实现模态框管理的例子。
先来看看如何使用Context来渲染模态框, 很简单, ModalContext.Provider给下级组件暴露一个render方法,通过这个方法来传递需要渲染的模态框组件和props:
export interface BaseModalProps { visible: boolean onHide: () => void } interface ModalContextValue { render(Component: React.ComponentType<any >, props: any ): void } const Context = React.createContext<ModalContextValue>({ render: () => { throw new Error ("useModal 必须在ModalRenderer 下级" ) }, }) const ModalRenderer: FC<{}> = props => { const [modal, setModal] = useState< | { Comp: React.ComponentType<any >; props: any ; visible?: boolean } | undefined >() const hide = useCallback(() => { setModal(prev => prev && { ...prev, visible: false }) }, []) const render = useCallback<ModalContextValue["render" ]>((Comp, props ) => { setModal({ Comp, props, visible: true } ) }, [] ) const value = useMemo (( ) => ({render} ), [] ) return ( <Context.Provider value={value}> {props.children} <div className="modal-container"> {} {!!modal && React.createElement(modal.Comp, { ...modal.props, visible: modal.visible, onHide: hide, } )} </div> </Context.Provider> )}
再看看Hooks的实现, 也很简单,就是使用useContext来访问ModalContext, 并调用render方法:
export function useModal <P extends BaseModalProps >( Modal: React.ComponentType<P>, ) { const renderer = useContext(Context) return useCallback( (props: Omit<P, keyof BaseModalProps>) => { renderer.render(Modal, props || {}) }, [Modal], ) }
应用示例:
const MyModal: FC<BaseModalProps & { a: number }> = props => { return ( <Modal visible={props.visible} onOk={props.onHide} onCancel={props.onHide}> {props.a} </Modal> ) } const Home: FC<{}> = props => { const showMyModal = useModal(MyModal) const handleShow = useCallback(() => { / / 显示模态框 showMyModal({ a: 123, }) }, []) return ( <div> showMyModal: <button onClick={handleShow}>show</ button> </div> ) }
可运行的完整示例可以看这里
⤴️回到顶部
React Hooks 技术地图 全家桶和Hooks的结合 :
一些有趣的Hooks集合 :
Awesome
FAQ
总结 本文篇幅很长、代码很多。能滑到这里相当不容易, 给你点个赞。
你用React Hook遇到过什么问题? 开过什么脑洞,下方评论告诉我.
欢迎关注我, 和我交流. 我有社恐, 但想多交些圈内朋友(atob('YmxhbmstY2FybmV5')
, 备注掘金,我不喝茶,近期也不换工作)
本文完!