友好的 React Hooks
网络上对 react hooks 的评价负面大于正面,确实很容易写出性能有问题的代码,关键就在于:我们太喜欢用 useState 了。
在 vue-composition-api 中,reactivity 数据都有 wrapper,custom-vca 里不管产生多少个 reactivity 对象,不会直接产生 re-render。只有那些被 return 到外部跟 template 绑定的部分才会触发视图渲染。
而 react 的 reactivity 就是通过 re-render 实现的,useState 没有 wrapper,每次使用都会得到一个触发渲染的函数。在这种 reactivity 机制下,就需要特殊的方式编写 hooks —— State/Effect 分层
假设有个 useHeight:
1
| const [ref, height] = useHeight()
|
高度变化时,被动 re-render,难以转换合并。大部分情况下,不提供 state,而提供 effect 可能会更好:
1 2 3 4 5
| const [height, setHeight] = useState(0) const ref = useHeight((height: number) => { setHeight(height) })
|
使用者在外部声明 state,然后在 callback 中按需 setState。使用者可以结合其他 state,做 dispatch 到 reducer 的一次整体更新,而不是被动 re-render。
根据 State/Effect 分层理念,尝试着给出友好地 react hooks 公式:
1
| const handler = useProducer(consumer, options)
|
producer 接收 consumer callback 作为参数,返回 handler 控制函数,用于绑定到事件或其他位置。
React 实现 useHeight
给定一个 resizable 的 textarea,我们监听它的高度变化,并展示到文本里。同时给个 checkbox,用户决定是否继续监听。并且只监听一定范围内的尺寸变化。
代码实现按照 low-level -> high-level
首先实现一个 useResizeObserver,对 dom api 的 low-level 适配:
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
| import { useCallback, useLayoutEffect, useRef } from "react"
const useDispatch = <I extends any[], O>(f: (...args: I) => O): typeof f => { const dispatchRef = useRef<typeof f>(f) const callback = useCallback<typeof f>((...args) => { return dispatchRef.current(...args) }, [])
useLayoutEffect(() => { dispatchRef.current = f }, [f])
return callback }
export const useResizeObserver = <T extends HTMLElement>( callback: (target: T) => any ) => { const ref = useRef<T | null>(null) const observerRef = useRef<ResizeObserver | null>(null) const dispatch = useDispatch(callback)
const trigger = (elem: T | null) => { ref.current = elem
if (observerRef.current) { observerRef.current.disconnect() observerRef.current = null }
if (!elem) { return }
const observer = new ResizeObserver(() => { dispatch(elem) })
observer.observe(elem) observerRef.current = observer }
const enable = () => { if (ref.current) { observerRef.current?.observe(ref.current) } }
const disable = () => { if (ref.current) { observerRef.current?.unobserve(ref.current) } }
return { trigger, enable, disable } }
|
useResizeObserver 不返回 state 出去,而是暴露一个 resize effect。然后再使用 useResizeObserver 实现 useHeight:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export const useHeight = <T extends HTMLElement>( callback: (height: number) => any ) => { const heightRef = useRef<number>(0) const observer = useResizeObserver<T>((target) => { const height = target.offsetHeight heightRef.current = height callback(height) })
const getCurrentHeight = () => heightRef.current
return { ...observer, getCurrentHeight } }
|
同样的思路还能实现其他监听。
最后在页面中使用:
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
| export default function App() { const [height, setHeight] = useState(0) const [checked, setChecked] = useState(false)
const handler = (event: React.ChangeEvent<HTMLInputElement>) => { setChecked(event.target.checked) }
const observer = useHeight < HTMLTextAreaElement > ((currentHeight) => { if (currentHeight > 300) { if (currentHeight !== 300) { setHeight(300) } } else { setHeight(currentHeight) } })
useEffect(() => { if (checked) { observer.enable() } else { observer.disable } }, [checked, observer])
return ( <div> <textarea ref={observer.trigger} /> <div>height is {height}</div> <input type="checkbos" checked={checked} onChange={handler} /> </div> ) }
|