React18 的 useEffect 新特性为什么被疯狂吐槽?

大家好,我是零一,react18 已经出来一段时间了,create-react-app 默认安装的 React 版本也已经是 18+,不知道有没有小伙伴发现自己有点看不懂 React 了?

import { useEffect, useState } from 'react'

function App () {
  const [data, setData] = useState(0)
 
  useEffect(() => {
    setData(preData => preData + 1)
  }, [])
 
  return (
    <div>{data}</div>
  )
}
看一下这段简单的代码,页面最终展示的数字是几?

是 1 这样吗?我觉得应该也是这样,可事实就是在 React18 里,这并不是预期效果,最终展示的其实是 2,为什么呢?

useEffect 的"新特性"
根据 React 最新的文档[1] 中对于 useEffect 的介绍得知,之所以我们刚才的例子最终展示的是 2 而不是 1 的原因是,在 dev 环境下,React 会将每个组件挂载两次进行测试。测试什么?测试你的 useEffect 有没有潜在问题

大家都知道函数式组件挂载后,会执行 useEffect 定义的副作用;在组件卸载时,会执行 useEffect return 出来的回调执行一些组件卸载时的行为,即:

function App () {
  useEffect(() => {
    console.log('组件挂载了')
    return () => {
      console.log('组件卸载了')
    }
  }, [])
 
  return (
    <div>useEffect</div>
  )
}
从组件挂载到卸载就会依次打印:

组件挂载了
组件卸载了
而在 React18 里,是这样打印的:

组件挂载了
组件卸载了
组件挂载了
按照文档里所说的,之所有这么做的,是为了通过挂载两次组件来提早发现你的问题,例如:

import { useEffect, useState } from 'react'

function App () {
  const [data, setData] = useState(0)
 
  useEffect(() => {
    setInterval(() => {
      setData(preData => preData + 1)
    }, 1000)
  }, [])
 
  return (
    <div>{data}</div>
  )
}
这段代码时很多刚使用 React 的同学经常会犯的错误,在 useEffect 里定义了个定时器,但没有在任何地方去清除它,所以即使在组件卸载了,这个定时器仍然还在运作,不光造成了内存泄漏,还可能会导致程序出现问题

所以就基于这段错误的代码,React18 执行 挂载 => 卸载 => 挂载,你就会发现,实际是有两个定时器在跑的,所以原本你想每秒 data + 1,变成了每秒 data + 2,如此明显的问题一下就被我们发现了

那正确的做法就是在 useEffect 里 return 一个用于卸载时执行的回调函数:

import { useEffect, useState } from 'react'

function App () {
  const [data, setData] = useState(0)
 
  useEffect(() => {
+   const timer = setInterval(() => {
      setData(preData => preData + 1)
    }, 1000)
 
+   return () => clearInterval(timer)
  }, [])
 
  return (
    <div>{data}</div>
  )
}
这样就没有问题了。谢谢 React18 这个"独特"的新特性(手动狗头)

单单基于这个出发点,我觉得是非常好的,能帮我们提早发现问题,解决问题,而不是等发到线上后造成了性能问题,回过头来再逐一排查。而且这只会在开发环境才会挂载两次,生产环境还是正常的

但真的是个完美的特性吗?根据网友的吐槽和我目前使用下来的感受,给我们造成的麻烦可能大于它本身的好处了

即使我的 useEffect 里根本没有需要在卸载时清理的对象,它也会被执行两次,比如请求两次、赋值两次 ... 这似乎是给我们造成了不少的负担啊,不知道的以为是别的地方出了 bug 呢!

关闭特性
我也可以手动关闭这个特性,找到入口文件 main.tsx,把 StrictMode 标签给去掉就好了

mport React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
- <React.StrictMode>
    <App />
- </React.StrictMode>
)
不过这样也把其它的提示给一并干掉了,其实我是不想这么做的

只是这样?
有很多人都在吐槽着!比如:初始化时 useEffect 会造成两次请求的话,似乎我们也不该在 useEffect 中发起请求?

在这里插入图片描述
然而 Dan 给出的解释就是说,你应该在服务端渲染时就请求到数据,而不是在客户端渲染挂载了 DOM 后才请求数据

其实 React18 将在之后推出一些别的功能,这个模拟组件重新挂载的特性只是为之后的功能做准备的,具体是什么功能呢?类似于 Vue 的 KeepAlive[2]

最后
简单总结一下:这个特性出发点是好的,同时也是为了之后的新特性做准备。但推出这个功能的同时也要考虑一下开发者的体验(起码是大部人的开发体验),不然真的是得不偿失。



作者:零一


欢迎关注微信公众号 :前端印象