React 哲学号称一套一套的,我也总结过一部分。React 的某些设计确实是挺巧妙的,但这只是 React 的其中一面。
某些情况下,这些说辞真的是一种对 React 的“自适应”,换言之,找理由来掩饰 React 的设计缺陷。在某些场景下,这些设计缺陷会变得很难绕过,甚至绕不过,要不怎么说用 React 心智负担高呢。
事件的依赖
useEffect 依赖管理十分困难这件事众所周知,这个问题也算是其中一个特别难解决的分支。
举个例子,这里有一个非 React 原生库,想要包装成 React 组件。
viselect/packages/react/src/SelectionArea.tsx at master · simonwep/viselect

可以看到作者在封装的时候是没有把 useEffect
里所有使用的值都加入依赖,直接当初始化使用。当然一般情况下是没有问题的,但是如果你的传入的 props.onStart
里面引用了 useState
的变量呢?
那就完了。它不会生效的,因为该state更新时,这个 useEffect
根本不会更新,所以拿到的永远只有最开始的值。
看例子,注意图中高亮区域:

如果把 props.onStart
加入了依赖又会怎么样呢?当然可以的,但是这又意味着整个初始化过程会重复运行,这简直毫无意义,纯纯的嫌用户电脑性能太高。
那到底怎么办?你可以把函数里面用到的会变化的值统统换成 ref
,这样能用,不过非常难看,我们接着看。
其实有比较官方、也优雅一点的办法的:使用 useEffectEvent,至少它现在还叫这个名字,因为 React 到 19 发布为止,它都还是个实验性功能。
useEffectEvent
里面的函数不需要写依赖,依然能让所有使用的值是最新的。使用的场景就基本针对于上述的“事件触发”场景。
官网给出的示例代码:
function ChatRoom({ roomId, theme }) { const onConnected = useEffectEvent(() => { showNotification('Connected!', theme); });
useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on('connected', () => { onConnected(); }); connection.connect(); return () => connection.disconnect(); }, [roomId]); // ✅ All dependencies declared // ...
清理顺序
useEffect() parent-child cleanup order · Issue #16728 · facebook/react
useEffect
的清理看起来很优雅,所有相关代码都内聚在一个函数里。
然而,React 清理副作用时的顺序有点任性,有可能你在父组件清除函数已经把你要用的东西 destroy
了,子组件的清除函数还使用了被父组件 destroy
的对象,于是产生了很奇妙的错误,这又是一个对非 React 原生库接入非常不友好的一个点。
而且,这个问题 React 团队甚至是不打算解决的。一般情况,你可以冒着内存泄漏的风险不管它,如果你一定要解决的话,你就得自己写 workaround 来处理全局的 destroy 了。
所以
虽然 React 确实好用,但是在某些情况,例如你使用了非 React 原生的 UI 或事件库,确实不好用(废话文学)。