未来,React 将提供一个功能,让组件在卸载之间保持状态。为了做好准备,React 18 为严格模式引入了一个新的仅限开发的检查。 React 将自动卸载并重新安装每个组件,每当一个组件第一次挂载时,在第二次挂载时恢复之前的状态。如果这会破坏您的应用程序,请考虑删除严格模式,直到您可以修复组件以恢复以现有状态重新安装。

要点

根据 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 zwz100034zwz100038 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变为popotpotapotatpotato。这可能会为这些值中的每一个启动不同的提取,但不能保证它们会按该顺序返回。因此,显示的结果可能是错误的(之前的任何查询)。因此,这里需要进行清理,以确保显示的结果不会过时,并防止出现竞争条件:

功能结果({查询}){

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的推文

喜欢吗?查看我的博客或在我的博客上阅读这篇文章

Logo

React社区为您提供最前沿的新闻资讯和知识内容

更多推荐