前端知库

useInsertionEffect 为什么是给 CSS-in-JS 库用的?和 useLayoutEffect、useEffect 时机有什么不同?

useInsertionEffect 保证在所有 layout Effect 之前执行,CSS-in-JS 库在这里插入 <style>,其他组件在 useLayoutEffect 里测量布局时样式已经到位。它不能更新 state,执行时 ref 还没绑定,DOM 也不保证已更新,只在客户端运行。应用代码基本用不到它。

参考答案

简答

运行时 CSS-in-JS 库(在组件渲染时才生成样式的那类)要把 <style> 标签插进页面。useInsertionEffect 就是专门给它们插样式用的 Hook,它保证在任何 useLayoutEffect 之前执行。

为什么不能在别处插?在渲染过程中插最糟,过渡更新时浏览器可能每一帧都要重新计算样式。放到 useLayoutEffect 或 useEffect 又太晚,别的组件的 layout Effect 可能已经在测量布局了,量到的是没有样式时的尺寸。useInsertionEffect 正好排在这两者前面。

三种 Effect 的顺序是:useInsertionEffect 最先,然后 useLayoutEffect,它在浏览器绘制前执行;useEffect 最后,通常在绘制后。

它的限制很多:不能在里面更新 state,执行时 ref 还没绑定,DOM 可能更新了也可能没更新,服务端渲染时不运行。所以 React 文档明确说,它是给 CSS-in-JS 库作者用的,写业务代码时应该用 useEffect 或 useLayoutEffect。

CSS-in-JS 库怎样用它

React 文档给的示意写法是这样的:一个 useCSS Hook 收到一条样式规则,在 useInsertionEffect 里检查是否已经插过,没插过就往 <head> 里加一个 <style>。

JavaScript
const isInserted = new Set();

function useCSS(rule) {
  useInsertionEffect(() => {
    if (!isInserted.has(rule)) {
      isInserted.add(rule);
      document.head.appendChild(getStyleForRule(rule));
    }
  });
  return rule;
}

function Button() {
  const className = useCSS('...');
  return <div className={className} />;
}

为什么用一个模块级的 Set 去重?因为同一条规则可能被很多组件、很多次渲染用到,<style> 只需要插一次。getStyleForRule 是示意函数,代表“把规则变成 style 元素”这一步。

为什么放在 useLayoutEffect 里不行

设想页面上有一个提示框组件,它在 useLayoutEffect 里量自己的高度,再决定显示在上方还是下方,这正是《useEffect 和 useLayoutEffect 在什么时候执行?测量 DOM 为什么要用 useLayoutEffect?》里讲的场景。

如果某个 CSS-in-JS 库也在 useLayoutEffect 里插样式,两者的先后就说不准了。提示框可能先量,那时它的 padding、字号还没生效,量出来的高度是错的,位置也就算错了。

useInsertionEffect 的唯一保证就是在所有 layout Effect 之前执行。所以样式插在这里,任何组件的 layout Effect 开始测量时,<style> 已经在页面上了。

时机上还有哪些和其他 Effect 不同

文档只承诺“在 layout Effect 之前”,没有承诺和 DOM 更新的先后,原话是它可能在 DOM 更新之前或之后运行,不应依赖某个时间点的 DOM 状态。这也是为什么它不能读 ref,ref 这时还没绑定。

清理和重新执行的顺序也不一样。其他 Effect 是先把所有要清理的清理完,再统一执行新的 setup。useInsertionEffect 是一个组件一个组件地处理,同一个组件的清理和 setup 紧挨着执行,所以不同组件之间会交错。

另外它在服务端不运行。CSS-in-JS 库要支持服务端渲染,得在渲染时自己收集用到的规则,然后输出到 HTML 里。文档的示意写法是在 useCSS 里判断 typeof window === 'undefined',把规则加进一个收集用的 Set。

常见追问

为什么在 useInsertionEffect 里不能 setState?

文档把这条列为明确限制:它不能用来调度更新。它的用途只有一个,就是在布局测量之前把样式插进页面,状态更新应该放在其他 Effect 或事件处理函数里。

有了它,运行时 CSS-in-JS 就没有性能问题了吗?

没有。React 文档说运行时注入样式有两个问题:浏览器要更频繁地重新计算样式,以及在错误时机注入会很慢。useInsertionEffect 只解决第二个。所以 React 更推荐静态样式写在 CSS 文件里,动态样式用内联 style。

业务代码里想在绘制前改 DOM,能用它吗?

不该用。它执行时 DOM 不一定已更新、ref 也没绑定,拿不到可靠的元素。绘制前读写 DOM 应该用 useLayoutEffect,其余情况用 useEffect,依赖和清理写法见《useEffect 的依赖数组和清理函数应该怎么写?》。