原文链接 React Tricks: Fast, Fit and Fun

Wouter库的作者记录了他在开发和维护 Wouter时所使用的一些 React技巧,这其中主要是一些用来性能优化的技巧。因为 Wouter 是一个微型的 React路由库,整个包的大小不到 2kb,但是却拥有很多路由功能,作者分享的这些技巧对微型React库的开发非常有用

组件组合与 React.cloneElement

这个API日常使用的可能不太多,但是在一些场景下它非常有用,作者介绍了几种使用的场景

asChild

在组件封装中,asChild是一种常用的替换组件底层元素的模式。可以用来完全自定义组件的底层元素类型

export default function Button({ children, type, size, className, onClick, asChild, ...restProps }) {
  const _className = cn(_button({ type, size, className }));
  const _props = { className: _className, onClick, ...restProps };
 
  if (asChild) {
    if (React.isValidElement(children) === false) {
      throw new Error('children必须是一个React element');
    }
    return React.cloneElement(children, _props);
  }
  return <button {..._props}>{children}</button>;
}
// <a href="/">Hi!</a>
<Link to="/">Hi!</Link> 
 
// ⛔️ <a href="/"><a /></a>
<Link to="/"><StyledLink /></Link>
 
// 👍 <a href="/" />
<Link to="/" asChild><StyledLink /></Link>

覆盖引用

在组件内部可以通过 cloneElement来覆盖 children的 ref 引用。获取引用之后做一针对用户传入 children的操作

const RenderIsExpensive = ({ children }) => {
  const targetRef = useRef(null)
 
	const emitParticles = () => {
    // some magic over `targetRef.current` 
 	}
 
	useEffect(() => {
	  emitParticles()
	}) // no deps, fires after every render
 
  return cloneElement(children, { ref: targetRef })
}
 
// usage:
<RenderIsExpensive>
  <Button>Counter: {counter}</Button>
</RenderIsExpensive>

useMemo & memo

作者解释了如何使用 useMemomemo 来避免 React 组件的额外渲染

React.memo 默认是使用 Object.is来比较先前的值和当前的值,这对于基础类型的属性来说是没有问题的,但是对于将 匿名函数 或者 非常量数组作为 props传递,则会无效

const Fast = React.memo(SlowComponent) 
 
// 错误的
<Fast onDone={() => { ... }} />
<Fast severity={["warning", "error"]} />

如何稳定对象引用的方法

  • 根据依赖列表计算值,但不能保证会重新计算
  • 将静态函数、对象和数组移动到全局命名空间
  • 使用 useRefuseStateuseEvent

从不更新的 useState

可以通过创建一个不包含更新函数的 state来保存一些值

const [value] = useState(() => {
	// 初始value
})

这种通过 useState 创建的值,在组件初始化时只会执行一次,并且对 value的引用在组件的生命周期内不会改变,我们可以利用这个特性,在组件初始化之前执行代码,而不是在任何渲染行为发生之前 useLayoutEffectuseEffect

const useWindowTitle = (title) => {
   const [prevTitle] = useState(() => document.title)
 
   useEffect(() => {
			document.title = title
			return () => document.title = prevTitle
 
      // prev title isn't actually needed here, but linter will complain
      // wouter actually supresses these warnings to reduce checks and 
      // keep the size small
   }, [title, prevTitle]) 
}

这种方式还可以用于在应用程序启动时仅非配一次重型资源,并提供给底层组件,具体看文章

稳定的回调函数 useEvent

很常见的一种例子

const onChange = useCallback(() => {
	console.log(value)
}, [value]);
 
<Input onChange={(e) => setValue(e.target.value)} / >

这种写法对于性能优化来说结果并不太好,并且有可能还不如不优化,因为 onChange会在每次 setValue 之后都会是一个新的函数引用,因为 value 的值经常变化,所以 useCallback 并没有效果

可以使用 useEvent 来创建一个稳定的回调引用,它返回一个稳定的包装函数,内部始终调用狗子提供的最新回调函数,React 官方目前还没有明确提供这个hooks,但社区中存在一些可用的版本, 例如 GitHub - scottrippey/react-use-event-hook: Same as React’s useCallback, but returns a stable reference.
使用方式

import useEvent from "react-use-event-hook" // user-land shim
 
const [x, y] = useMousePosition(canvasRef)
                     
const leaveComment = useEvent((text) => {
  ...
}) // no deps!

leaveComment 始终保持相同的值,所以不会导致发生重新渲染

使用 useSyncExternalStore 来安全的订阅外部状态变化

|React Infographics
配合 useSyncExternalStore 使用
使用useGlobalHooks 在多个组件间共享状态