useEffect 在 React 18 中触发两次

要点
根据 React 18 变更日志:
未来,React 将提供一个功能,让组件在卸载之间保持状态。为了做好准备,React 18 为严格模式引入了一个新的仅限开发的检查。 React 将自动卸载并重新安装每个组件,每当一个组件第一次挂载时,在第二次挂载时恢复之前的状态。如果这会破坏您的应用程序,请考虑删除严格模式,直到您可以修复组件以恢复以现有状态重新安装。
因此,简而言之,当 Strict Mode 开启时,React 会安装组件两次(仅在开发中!)以检查并让您知道它有错误。这仅在开发中,对生产中运行的代码没有影响。
如果你只是来这里“知道”为什么你的效果会被调用两次,那就是要点。您可以花时间阅读整篇文章,然后去修复您的效果但是,您可以留在这里并了解一些细微差别。
但是首先,什么是效果?
根据 beta 反应文档:
某些组件需要与外部系统同步。例如,您可能希望根据 React 状态控制非 React 组件、设置服务器连接或在组件出现在屏幕上时发送分析日志。效果让您在渲染后运行一些代码,以便您可以将组件与 React 之外的某些系统同步。
这里的_after rendering_部分非常重要。因此,在向组件添加效果之前,您应该牢记这一点。例如,您可能正在根据本地状态或道具更改在效果中设置某些状态。
功能用户信息({名,姓}){
const [fullName, setFullName] u003d useState('') // 🔴 避免:冗余状态和不必要的效果
使用效果(()u003d> {
设置全名(`${firstName} ${lastName}`)
}, [firstName, lastName]) return <div>用户的全名:{fullName}</div>
}
只是不要。不仅没有必要,而且在渲染过程中已经计算了值时会导致不必要的第二次重新渲染
功能用户信息({名,姓}){
// ✅ 好:在初始渲染期间计算
const fullName u003d `${firstName} ${lastName}`return <div>用户全名:{fullName}</div>
}
“但是如果在渲染过程中计算一些值不如我们这里的fullName变量那么便宜怎么办?”好吧,在这种情况下,你可以memoize 一个昂贵的计算。你仍然不需要在这里使用 Effect
函数 SomeExpensiveComponent() {
// ... 常量数据 u003d useMemo(() u003d> {
// 除非 deps 改变,否则不会重新运行
return someExpensiveCalculaion(deps)
}, [deps]) // ...
}
这告诉 React 不要重新计算data除非deps发生变化。即使someExpensiveCalculaion非常慢(比如运行大约需要 10 毫秒),您也只需要这样做。但这取决于你。首先看看没有useMemo是否足够快,然后从那里开始构建。您可以使用zwz100035 console.time zwz100036 zwz100034或zwz100038 performance.now zwz100039 zwz100037检查运行一段代码所需的时间:
console.time('myBadFunc')
我的坏函数()
console.timeEnd('myBadFunc')
你可以看到像myBadFunc: 0.25ms这样的日志。您现在可以决定是否使用useMemo。此外,即使在使用zwz100041 React.memo zwz100042 zwz100040之前,您也应该首先阅读这篇很棒的文章byDan Abramov
什么是使用效果
useEffect是一个反应钩子可以让你在你的组件中运行副作用。如前所述,效果在渲染之后运行,并且是由渲染本身而不是由特定事件引起的。 (事件可以是用户图标,例如,单击按钮)。因此useEffect应该只用于同步,因为它不仅仅是即发即弃。 useEffect 主体在某种意义上是“反应性的”,只要依赖数组中的任何依赖项发生更改,就会重新触发效果。这样做是为了使运行该效果的结果始终保持一致和同步。但是,正如所见,这是不可取的。
在这里和那里使用效果可能非常诱人。例如,您想根据特定条件过滤项目列表,例如“成本低于 ₹500”。您可能会考虑为它编写一个效果,以便在项目列表更改时更新变量:
功能 MyNoobComponent({ 项目 }) {
const [filteredItems, setFilteredItems] u003d useState([]) // 🔴 不要使用效果来设置派生状态
使用效果(()u003d> {
setFilteredItems(items.filter(item u003d> item.price < 500))
}, [项目]) //...
}
如前所述,它是低效的。在更新状态和计算和更新 UI 之后,React 将需要重新运行你的效果。由于这一次我们正在更新一个状态(filteredItems),React 需要从第 1 步重新开始所有这个过程!为了避免所有这些不必要的计算,只需在渲染期间计算过滤列表:
功能 MyNoobComponent({ 项目 }) {
// ✅ 好:在渲染期间计算值
常量过滤项目 u003d items.filter(item u003d> item.price < 500) //...
}
所以,的经验法则:当可以从现有的 props 或 state 计算出一些东西时,不要把它放在 state 中。相反,在渲染期间计算它。这使您的代码更快(您避免了额外的“级联”更新),更简单(您删除了一些代码),并且更不容易出错(您避免了由不同状态变量彼此不同步引起的错误)。如果您觉得这种方法很新,Thinking in React对应该进入状态有一些指导。
此外,您不需要效果来处理事件。 (例如,用户单击按钮)。假设您要打印用户的收据:
功能 PrintScreen({ billDetails }) {
// 🔴 不要对事件处理程序使用效果
使用效果(()u003d> {
if (billDetails) {
myPrettyPrintFunc(billDetails)
}
}, [billDetails]) // ...
}
我对过去编写这种类型的代码感到内疚。只是不要这样做。相反,在父组件中(您可能将billDetails设置为setBillDetails(),在用户单击按钮时,请帮自己一个忙并仅在此处打印):
功能父组件(){
// ... 返回 (
// ✅ 好:使用内部事件处理程序
<button onClicku003d{() u003d> myPrettyPrintFunc(componentState.billDetails)}>
打印收据
</按钮>
) // ...
}
上面的代码现在没有在错误的地方使用useEffect引起的错误。假设您的应用程序记住页面加载时的用户状态。假设用户由于某种原因关闭了选项卡,然后返回,只是在屏幕上看到一个打印弹出窗口。这不是一个好的用户体验。
每当您考虑代码应该在事件处理程序中还是在useEffect中时,请考虑为什么需要运行此代码。这是因为屏幕上显示的内容,还是用户执行的某些操作(事件)。如果是后者,只需将其放入事件处理程序中。在我们上面的例子中,打印应该是因为用户点击了一个按钮,而不是因为屏幕转换或向用户显示的东西。
获取数据
获取数据中最常用的效果用例之一。到处都在使用它作为componentDidMount的替代品。只需将一个空数组传递给依赖项数组,仅此而已:
使用效果(()u003d> {
// 🔴 不要 - 在 useEffect 中获取数据 _without_ 清理
常量 f u003d async () u003d> {
设置加载(真)
尝试 {
const res u003d 等待 getPetsList()
setPetList(res.data)
} 抓住 (e) {
控制台错误(e)
} 最后 {
设置加载(假)
}
} F()
}, [])
我们以前都见过并且可能写过这种类型的代码。好吧,有什么问题?
-
首先,
useEffects 只是客户端。这意味着它们不在服务器上运行。因此,最初呈现的页面将只包含一个 HTML 外壳,可能还有一个微调器 -
这段代码容易出错。例如,如果用户返回,单击返回按钮,然后再次重新打开页面。第一个在第二个之前触发的请求很可能在之后得到解决。所以,我们状态变量中的数据将是陈旧的!在这里,在上面的代码中,这可能不是一个大问题,但它是在不断变化的数据的情况下,或者例如在输入输入时根据搜索参数查询数据;这是。因此,在效果中获取数据会导致竞争条件。您可能在开发中甚至在生产中都看不到它,但请放心,您的许多用户肯定会体验到这一点。
-
useEffect不负责缓存、后台更新、陈旧数据等,这些在非业余应用程序中是必需的。 -
这需要大量的样板来手工编写,因此不容易管理和维护。
好吧,这是否意味着任何获取都不应该发生在效果中,不:
功能产品页面(){
使用效果(()u003d> {
// ✅ 这个逻辑应该运行在一个 effect 中,因为它在页面显示时运行
发送分析({
页面:window.location.href,
事件:'反馈\表格',
})
}, []) useEffect(() u003d> {
// 🔴 这个逻辑与触发事件的时间有关,
// 因此应该放在事件处理程序中,而不是放在效果中
如果(productDataToBuy){
进行结账(productDataToBuy)
}
}, [productDataToBuy]) // ...
}
发出的分析请求可以保存在useEffect中,因为它会在页面显示时触发。在严格模式下,在 React 18 的开发中,useEffect 会触发两次,但这很好。 (看这里如何处理那个)
在许多项目中,您可以将效果视为将查询同步到用户输入的一种方式:
功能结果({查询}){
const [res, setRes] u003d useState(null) // 🔴 获取而不清理
使用效果(()u003d> {
获取(`results-endpoint?query=${query}}`).then(setRes)
}, [询问]) // ...
}
也许这似乎与我们之前讨论的相反:将获取逻辑放入事件处理程序中。但是,这里的查询可能来自任何来源(用户输入、url 等)。因此,结果需要是_synced_和query变量。但是,考虑我们之前讨论的情况,用户可能会先按下后退按钮,然后再按下前进按钮;那么res状态变量中的数据可能是陈旧的,或者考虑来自用户输入和用户快速输入的query。查询可能会从p变为po到pot到pota到potat到potato。这可能会为这些值中的每一个启动不同的提取,但不能保证它们会按该顺序返回。因此,显示的结果可能是错误的(之前的任何查询)。因此,这里需要进行清理,以确保显示的结果不会过时,并防止出现竞争条件:
功能结果({查询}){
const [res, setRes] u003d useState(null) // ✅ 通过清理获取
使用效果(()u003d> {
let done u003d false fetch(`results-endpoint?query=${query}}`).then(data u003d> {
如果(!完成){
设置资源(数据)
}
}) 返回 () u003d> {
完成u003d真
}
}, [询问]) // ...
}
这确保只接受所有响应中的最新响应。仅仅处理带有效果的竞争条件似乎需要做很多工作。但是,数据获取还有很多其他内容,例如缓存、重复数据删除、处理状态数据、后台获取等。您的框架可能会提供比使用useEffect更有效的内置数据获取机制。
如果您不想使用框架,可以将上述所有逻辑提取到自定义钩子中,或者可以使用库,例如TanStack Query(以前称为 useQuery)或swr。
到目前为止
-
useEffect在严格模式下在开发中触发两次,以指出生产中会有错误。 -
useEffect应该在组件需要与某些外部系统同步时使用,因为在渲染过程中不会触发效果并因此选择退出 React 的范例。 -
不要对事件处理程序使用效果。
-
不要对派生状态使用效果。 (哎呀,尽可能不要使用派生状态,并在渲染期间计算值)。
-
取数据时不要使用效果。如果您处于绝对无法避免这种情况的情况,至少在效果结束时进行清理。
致谢:
上面的大部分内容都是无耻的灵感来自:
-
Beta React 文档
-
Dan Abramov的推文
喜欢吗?查看我的博客或在我的博客上阅读这篇文章
更多推荐
所有评论(0)