使用 useEffect,如何跳过在初始渲染时应用效果?
Posted
技术标签:
【中文标题】使用 useEffect,如何跳过在初始渲染时应用效果?【英文标题】:With useEffect, how can I skip applying an effect upon the initial render? 【发布时间】:2019-04-10 06:28:10 【问题描述】:使用 React 的新效果挂钩,如果某些值在重新渲染之间没有改变,我可以告诉 React 跳过应用效果 - React 文档中的示例:
useEffect(() =>
document.title = `You clicked $count times`;
, [count]); // Only re-run the effect if count changes
但上面的示例将效果应用于初始渲染,以及count
已更改的后续重新渲染。如何告诉 React 跳过初始渲染的效果?
【问题讨论】:
你看过React.useMemo
吗?
【参考方案1】:
如指南所述,
Effect Hook,useEffect,增加了从函数组件执行副作用的能力。它的用途与 React 类中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 相同,但统一为一个 API。
在本指南的示例中,预计 count
仅在初始渲染时为 0:
const [count, setCount] = useState(0);
所以它将作为componentDidUpdate
工作并进行额外检查:
useEffect(() =>
if (count)
document.title = `You clicked $count times`;
, [count]);
这基本上是可以用来代替useEffect
的自定义钩子的工作方式:
function useDidUpdateEffect(fn, inputs)
const didMountRef = useRef(false);
useEffect(() =>
if (didMountRef.current)
return fn();
didMountRef.current = true;
, inputs);
感谢 @Tholle 建议 useRef
而不是 setState
。
【讨论】:
useRef
优于 setState
的建议在哪里?我在此页面上没有看到它,我想了解原因。
@rob-gordon 这是更新答案后删除的评论。原因是 useState 会导致不必要的组件更新。
这种方法有效,但它违反了 react-hooks/exhaustive-deps linter 规则。 IE。 fn 未在 deps 数组中给出。有人有不违反 React 最佳实践的方法吗?
@JustinLang React linter rules != 最佳实践,他们只尝试解决常见的钩子问题。 ESLint 规则并不智能,可能会导致误报或误报。只要钩子背后的逻辑是正确的,就可以使用 eslint-disable 或 eslint-disable-next 注释安全地忽略规则。在这种情况下,fn
不应作为输入提供。请参阅说明,reactjs.org/docs/…。如果fn
内部的某些东西引入了deps,则更像是应该直接将它们提供为inputs
。
注意:如果您使用多个 useEffects 检查 didMountRef,请确保只有最后一个(底部)将 didMountRef 设置为 false。 React 按顺序遍历 useEffects!【参考方案2】:
这是一个自定义钩子,它只提供一个布尔标志来指示当前渲染是否是第一个渲染(当组件被安装时)。它与其他一些答案大致相同,但您可以在 useEffect
或渲染函数或您想要的组件中的任何其他位置使用标志。也许有人可以提出一个更好的名字。
import useRef, useEffect from 'react';
export const useIsMount = () =>
const isMountRef = useRef(true);
useEffect(() =>
isMountRef.current = false;
, []);
return isMountRef.current;
;
你可以像这样使用它:
import React, useEffect from 'react';
import useIsMount from './useIsMount';
const MyComponent = () =>
const isMount = useIsMount();
useEffect(() =>
if (isMount)
console.log('First Render');
else
console.log('Subsequent Render');
);
return isMount ? <p>First Render</p> : <p>Subsequent Render</p>;
;
如果你有兴趣,这里有一个测试:
import renderHook from '@testing-library/react-hooks';
import useIsMount from '../useIsMount';
describe('useIsMount', () =>
it('should be true on first render and false after', () =>
const result, rerender = renderHook(() => useIsMount());
expect(result.current).toEqual(true);
rerender();
expect(result.current).toEqual(false);
rerender();
expect(result.current).toEqual(false);
);
);
我们的用例是隐藏动画元素,如果初始道具表明它们应该被隐藏。如果道具发生变化,在以后的渲染中,我们确实希望元素动画出来。
【讨论】:
导入从react-hooks-testing-library
更改为@testing-library/react-hooks
github.com/testing-library/react-hooks-testing-library/releases/…
为什么选择“isMount”而不是“didMount”或“isMounted”?
猜猜我在考虑当前渲染是否是发生安装的渲染。是的,我同意,现在回到它听起来有点奇怪。但它在第一次渲染时是真的,之后是假的,所以你的建议听起来会产生误导。 isFirstRender
可以工作。
感谢钩子!我同意@ScottyWaggoner,isFirstRender
是一个更好的名字
添加测试是 FIRE!反响很好。【参考方案3】:
我找到了一个更简单的解决方案,不需要使用另一个钩子,但是它有缺点。
useEffect(() =>
// skip initial render
return () =>
// do something with dependency
, [dependency])
这只是一个例子,如果您的情况很简单,还有其他方法可以做到这一点。
这样做的缺点是不能产生清理效果,只会在依赖数组第二次发生变化时执行。
不建议使用,您应该使用其他答案所说的内容,但我只是在这里添加了这个,所以人们知道这样做的方法不止一种。
编辑:
为了更清楚,您不应该使用这种方法来解决问题中的问题(跳过初始渲染),这仅用于教学目的,表明您可以做到同一件事以不同的方式。 如果您需要跳过初始渲染,请使用其他答案的方法。
【讨论】:
我刚刚学到了一些东西。我不认为这会起作用,然后我尝试了它,它确实起作用了。谢谢! React 应该为此提供更好的方法,但是这个问题在 Github 上是开放的,并且建议自己编写一个自定义解决方案(这完全是无意义的)。 那个的问题是它也会在unMount上执行,并不完美。 @ncesar 如果这是唯一有效的答案,可能你做错了什么,因为另一个答案是正确的,而我的只是为了教学目的【参考方案4】:我使用常规状态变量而不是 ref。
// Initializing didMount as false
const [didMount, setDidMount] = useState(false)
// Setting didMount to true upon mounting
useEffect(() => setDidMount(true) , [])
// Now that we have a variable that tells us wether or not the component has
// mounted we can change the behavior of the other effect based on that
const [count, setCount] = useState(0)
useEffect(() =>
if (didMount) document.title = `You clicked $count times`
, [count])
我们可以像这样将 didMount 逻辑重构为自定义钩子。
function useDidMount()
const [didMount, setDidMount] = useState(false)
useEffect(() => setDidMount(true) , [])
return didMount
最后,我们可以像这样在我们的组件中使用它。
const didMount = useDidMount()
const [count, setCount] = useState(0)
useEffect(() =>
if (didMount) document.title = `You clicked $count times`
, [count])
更新使用 useRef 挂钩来避免额外的重新渲染(感谢@TomEsterez 的建议)
这次我们的自定义钩子返回一个函数,返回我们的 ref 的当前值。你也可以直接使用 ref,但我更喜欢这个。
function useDidMount()
const mountRef = useRef(false);
useEffect(() => mountRef.current = true , []);
return () => mountRef.current;
用法
const MyComponent = () =>
const didMount = useDidMount();
useEffect(() =>
if (didMount()) // do something
else // do something else
)
return (
<div>something</div>
);
顺便说一句,我从来没有使用过这个钩子,而且可能有更好的方法来处理这个问题,这将更符合 React 编程模型。
【讨论】:
useRef
更适合这种情况,因为useState
会导致组件的额外且无用的渲染:codesandbox.io/embed/youthful-goldberg-pz3cx
您的useDidMount
函数中有错误。您必须用大括号 ...
将 mountRef.current = true
封装在 useEffect()
中。离开他们就像写return mountRef.current = true
,这会导致像An effect function must not return anything besides a function, which is used for clean-up. You returned: true
这样的错误【参考方案5】:
一个 TypeScript 和 CRA 友好的钩子,将其替换为 useEffect
,这个钩子的工作方式类似于 useEffect
,但不会在第一次渲染发生时触发。
import * as React from 'react'
export const useLazyEffect:typeof React.useEffect = (cb, dep) =>
const initializeRef = React.useRef<boolean>(false)
React.useEffect((...args) =>
if (initializeRef.current)
cb(...args)
else
initializeRef.current = true
// eslint-disable-next-line react-hooks/exhaustive-deps
, dep)
【讨论】:
【参考方案6】:这是我基于 Estus Flask 的 answer 用 Typescript 编写的实现。它还支持清理回调。
import DependencyList, EffectCallback, useEffect, useRef from 'react';
export function useDidUpdateEffect(
effect: EffectCallback,
deps?: DependencyList
)
// a flag to check if the component did mount (first render's passed)
// it's unrelated to the rendering process so we don't useState here
const didMountRef = useRef(false);
// effect callback runs when the dependency array changes, it also runs
// after the component mounted for the first time.
useEffect(() =>
// if so, mark the component as mounted and skip the first effect call
if (!didMountRef.current)
didMountRef.current = true;
else
// subsequent useEffect callback invocations will execute the effect as normal
return effect();
, deps);
现场演示
下面的现场演示演示了 useEffect
和 useDidUpdateEffect
钩子之间的区别
【讨论】:
【参考方案7】:让我给你介绍react-use。
想跑:
仅在第一次渲染后? useUpdateEffect
只有一次? useEffectOnce
是第一次挂载吗? useFirstMountState
想要通过深度比较、浅比较或节流运行效果?更多here。
不想安装库?检查code 并复制。 (也可以为那里的好人提供star
)
【讨论】:
【参考方案8】:以下解决方案与上面类似,只是我更喜欢更清洁的方式。
const [isMount, setIsMount] = useState(true);
useEffect(()=>
if(isMount)
setIsMount(false);
return;
//Do anything here for 2nd render onwards
, [args])
【讨论】:
这不是一个理想的方法,因为设置状态会导致另一个渲染【参考方案9】:我打算对当前接受的答案发表评论,但空间不足!
首先,在使用功能组件时,不要考虑生命周期事件,这一点很重要。考虑道具/状态的变化。我遇到过类似的情况,我只希望在特定道具(在我的情况下为 parentValue
)从其初始状态更改时触发特定的 useEffect
函数。因此,我创建了一个基于其初始值的 ref:
const parentValueRef = useRef(parentValue);
然后在useEffect
fn 的开头包含以下内容:
if (parentValue === parentValueRef.current) return;
parentValueRef.current = parentValue;
(基本上parentValue
没有变化就不要运行效果了,如果有变化就更新ref,准备下一次检查,继续运行效果)
因此,尽管建议的其他解决方案可以解决您提供的特定用例,但从长远来看,它将有助于改变您对功能组件的看法。
将它们视为主要基于某些道具渲染组件。
如果您确实需要一些本地状态,那么 useState
会提供,但不要假设您的问题将通过存储本地状态来解决。
如果你有一些代码会在渲染过程中改变你的道具,这个“副作用”需要被包裹在 useEffect
中,但这样做的目的是获得一个不受渲染时发生变化。 useEffect
钩子将在渲染完成后运行,并且正如您所指出的,它在每次渲染时运行 - 除非第二个参数用于提供道具/状态列表以识别哪些更改的项目将导致它将在以后运行。
祝您在功能组件/钩子之旅上好运!有时有必要忘记一些东西来掌握一种新的做事方式:) 这是一本优秀的入门书:https://overreacted.io/a-complete-guide-to-useeffect/
【讨论】:
以上是关于使用 useEffect,如何跳过在初始渲染时应用效果?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 react 中的新路由更改上使用 useEffect 重新渲染变量
如何在 React Native 应用程序中使用 React hook useEffect 为每 5 秒渲染设置间隔?