我用React 18 alpha重构了一个项目。过程中踩了很多坑。有些坑让我熬了好几个夜。本文是我的实战踩坑记录。需要说明的是React 18还是alpha版本。最终特性可能会有变化。
一、项目背景
先说说为什么要用React 18 alpha。
我们有一个数据可视化的项目。界面比较复杂。有大量的图表和表格。用户交互很多。用React 17的时候。有时候会卡顿。特别是在输入框输入的时候。图表会跟着更新。导致输入很卡。
React 18 alpha发布之后。我看到并发渲染的特性。觉得可以解决我们的卡顿问题。并发渲染可以让React同时准备多个版本的UI。把不紧急的更新推迟。让紧急的更新优先执行。这样用户输入就不会被阻塞了。
于是我就在一个分支上。把项目升级到了React 18 alpha。想试试并发渲染的效果。结果一升级就踩了很多坑。本文就是这些坑的记录。
二、并发渲染的坑
并发渲染是React 18最大的新特性。也是坑最多的地方。
坑1:渲染可以被中断
在React 17及之前。渲染是同步的。一旦开始渲染。就会一直执行完。不会被中断。
但是在React 18的并发模式下。渲染是可以被中断的。React渲染到一半。如果有更紧急的更新来了。就会暂停当前的渲染。先处理紧急的更新。然后再回来继续渲染。甚至可能丢弃之前的渲染结果。重新渲染。
这个变化带来了一个问题。就是渲染过程中的副作用可能会被执行多次。因为渲染被中断之后重新开始。之前执行的代码又会执行一遍。
比如你在render函数里调用了一个函数。这个函数有副作用。比如修改了一个全局变量。或者打印了日志。在并发模式下。这个函数可能会被调用多次。因为渲染被中断了又重新开始。
我们项目里有一个组件。在render的时候计算了一个值。并且把这个值存在了一个外部变量里。结果在并发模式下。这个值经常不对。因为render被中断了。外部变量被修改了多次。最后一次修改的值不是我们期望的。
解决方法是。render函数应该是纯函数。不要有副作用。所有副作用都放在useEffect或者useLayoutEffect里。不要在render的时候修改外部变量。我们把那个计算移到了useMemo里。问题就解决了。
坑2:useEffect的执行时机变了
在React 17里。useEffect是在浏览器绘制之后异步执行的。useLayoutEffect是在浏览器绘制之前同步执行的。
在React 18的并发模式下。useEffect的执行时机有一些变化。因为渲染可以被中断。useEffect可能会延迟执行。或者在某些情况下不执行。
我们项目里有一个组件。在useEffect里读取了DOM元素的尺寸。然后做一些计算。在React 17里没问题。在React 18里。有时候读取到的尺寸是0。因为DOM还没更新完。useEffect就执行了。
解决方法是。如果需要在DOM更新之后立即读取布局。用useLayoutEffect而不是useEffect。useLayoutEffect是同步执行的。在浏览器绘制之前执行。这时候DOM已经更新完了。可以正确读取尺寸。
还有一个坑是。在并发模式下。useEffect的清理函数可能会被调用多次。因为组件可能会被卸载又重新挂载。如果清理函数里有副作用。要注意幂等性。
三、自动批处理的坑
React 18的另一个新特性是自动批处理。在React 17里。只有事件处理函数里的状态更新会被批处理。Promise、setTimeout、原生事件里的更新不会被批处理。
在React 18里。所有的状态更新都会自动批处理。不管是在哪里触发的。这可以减少重新渲染的次数。提升性能。
坑3:依赖批处理的代码出问题了
我们项目里有一些代码。依赖了非批处理的行为。比如在一个Promise回调里。先更新state A。然后读取state A的值。再更新state B。
在React 17里。因为Promise回调里的更新不会被批处理。更新A之后组件会重新渲染。然后再更新B。所以读取A的时候能拿到最新的值。
但是在React 18里。因为自动批处理。A和B的更新会被合并成一次渲染。所以在更新B的时候。读取到的A还是旧的值。
这个问题导致我们的一些逻辑出错了。比如一个表单组件。先更新了表单数据。然后根据表单数据计算校验结果。结果校验结果用的是旧的数据。
解决方法是。不要依赖状态更新之后立即读取状态。应该用函数式更新。或者把需要的值存在变量里。比如先计算好新的A值。存在变量里。然后用这个变量来更新A和计算B。
坑4:测试出问题了
我们的单元测试也出了问题。有些测试在React 17里能通过。在React 18里失败了。
原因是测试里模拟了异步更新。比如用setTimeout或者Promise。在React 17里。这些更新不会被批处理。每次更新都会触发渲染。测试可以按顺序断言。
但是在React 18里。这些更新被批处理了。渲染的次数变少了。有些断言就失败了。因为期望的渲染次数不对。
解决方法是。更新测试用例。不要依赖具体的渲染次数。只断言最终的状态。或者用React Testing Library的waitFor来等待更新完成。
四、Suspense的坑
React 18改进了Suspense。支持了服务端渲染的Suspense。以及更多的使用场景。
坑5:Suspense的fallback闪烁
我们在项目里用了Suspense来做代码分割和数据加载。在React 17里。Suspense的表现基本正常。
但是在React 18 alpha里。Suspense有时候会出现fallback闪烁。就是内容已经加载好了。但是又突然显示fallback。然后又显示内容。
查了很久才发现。是因为并发渲染导致的。当一个组件正在加载的时候。如果有其他更新触发了重新渲染。Suspense的状态可能会被重置。导致fallback又显示出来。
解决方法是。给Suspense的子组件加key。或者把Suspense放在更稳定的位置。不要让它在每次渲染的时候都重新创建。还有就是尽量不要在Suspense的父组件里频繁更新状态。
这个问题在alpha版本里比较明显。希望正式版能修复。
坑6:数据获取的方式不对
React 18的Suspense支持数据获取。就是组件可以在render的时候"挂起"。等待数据加载完成。然后再渲染。
但是这个特性需要数据获取库的支持。比如Relay或者React Query的特定版本。我们最开始以为随便一个异步请求都能和Suspense配合。结果发现不行。
如果你用普通的useEffect加useState来获取数据。Suspense是检测不到的。不会触发挂起。只有用支持Suspense的数据获取库。或者自己实现Suspense兼容的数据获取。才能用这个特性。
我们最后用了React Query的实验版本。它支持Suspense模式。这样才能和Suspense配合使用。
五、startTransition的坑
startTransition是React 18的新API。可以把某些更新标记为非紧急的。让紧急的更新优先执行。
坑7:过度使用startTransition
最开始我觉得startTransition很好用。就把所有的更新都包在startTransition里。结果发现性能反而更差了。
因为startTransition会让更新延迟执行。如果所有更新都延迟了。界面的响应反而变慢了。用户点击按钮。要等一会儿才有反应。
后来我才明白。startTransition应该只用在那些不紧急的。可能会导致大量渲染的更新上。比如搜索框的输入。输入的时候要过滤大量数据。这种更新可以用startTransition。让输入框的响应优先。过滤结果延迟更新。
而像按钮点击、表单输入这种需要立即响应的更新。不要用startTransition。否则用户会觉得卡顿。
坑8:startTransition里的状态读取
和自动批处理类似。startTransition里的状态更新也是异步的。如果你在startTransition之后立即读取状态。读到的是旧值。
我们有一个组件。在startTransition里更新了筛选条件。然后立即用筛选条件去请求数据。结果请求用的是旧的筛选条件。
解决方法是。把筛选条件存在变量里。用这个变量去请求数据。而不是从state里读取。或者把请求逻辑放在useEffect里。监听筛选条件的变化。
六、useDeferredValue的坑
useDeferredValue是另一个新API。可以延迟更新某个值。让渲染优先处理更紧急的更新。
坑9:延迟值导致的不同步
useDeferredValue会返回一个延迟更新的值。在紧急更新的时候。这个值不会立即更新。会等紧急更新处理完之后再更新。
这就导致了一个问题。在同一次渲染里。原始值和延迟值可能不一样。比如你有一个输入框。输入的值是原始值。用useDeferredValue延迟之后的值用来过滤列表。在输入的时候。输入框显示的是最新的输入。但是列表还是旧的过滤结果。要等一会儿才更新。
这个行为是设计如此。但是如果处理不好。会让用户觉得界面不同步。
解决方法是。在使用延迟值的时候。给用户一个视觉提示。比如列表正在加载。或者显示一个骨架屏。让用户知道结果正在更新。
七、第三方库兼容的坑
坑10:第三方库不兼容
这是最大的坑。很多第三方库还没有适配React 18。在并发模式下会出问题。
比如我们用的一个状态管理库。在并发模式下。状态更新会出问题。因为它的内部实现依赖了同步渲染的假设。
还有一个UI组件库。在并发模式下。动画会出问题。因为动画的时序和渲染的时序不匹配了。
我们的解决方法是。对于不兼容的库。要么找替代品。要么不用并发模式。React 18可以不用并发模式。只是用它的其他新特性。比如自动批处理。这样大部分第三方库都能正常工作。
如果你想用并发模式。一定要仔细测试所有第三方库。确保它们在并发模式下正常工作。
八、给想尝试React 18的人的建议
如果你也想尝试React 18 alpha。我有几个建议。
1. 先在小项目上试
不要一上来就用在大项目上。先在小项目或者分支上试。熟悉新特性。踩踩坑。然后再考虑用在正式项目上。
2. 不要急着用并发模式
并发模式是最复杂的特性。坑也最多。可以先用React 18的其他特性。比如自动批处理。等熟悉了之后再开并发模式。
3. 确保render是纯函数
这是最重要的。在并发模式下。render可能会被中断和重新执行。所以render函数必须是纯函数。不能有副作用。所有副作用都放在effect里。
4. 充分测试
升级之后一定要充分测试。特别是交互复杂的组件。并发模式下很多行为和之前不一样。容易出bug。
5. 关注官方更新
alpha版本变化很快。API可能会改。bug也会不断修复。要关注官方的更新日志。及时升级。
九、写在最后
React 18 alpha有很多激动人心的新特性。并发渲染、自动批处理、Suspense改进等。这些特性能让React应用更流畅。用户体验更好。
但是alpha版本毕竟是alpha版本。坑很多。不适合用在生产环境。建议等正式版发布之后再用。
我这次踩坑的经历。让我对React 18有了更深的理解。也期待正式版的发布。
最后用一句话结束本文:"新特性很美好。但是踩坑是必经之路。"愿每一个前端开发者都能在踩坑中成长。做出更流畅的应用。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录