使用 useEffect,我如何跳过在初始渲染上应用效果?

使用 React 的新效果挂钩,我可以告诉 React 在重新渲染之间某些值没有改变的情况下跳过应用效果-React 文档中的例子:

useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

但是上面的例子在初始渲染和随后的重新渲染 count已经改变的地方应用了这种效果。如何告诉 React 跳过初始渲染的效果?

155432 次浏览

正如指南所说,

“效果钩子”,useEffect,增加了从功能组件执行副作用的能力。它的用途与 React 类中的 Component entDidMount、 Component entDidUpdate 和 Component entWillUnmount 相同,但是统一为一个 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

我使用常规状态变量而不是 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])

我们可以像这样将 did Mount 逻辑重构为一个自定义钩子。

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])

UPDATE 使用 useRef hook 来避免额外的重新呈现(感谢@TomEsterez 的建议)

这一次,我们的自定义钩子返回一个返回 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 编程模型。

这里有一个自定义钩子,它只提供一个布尔标志来指示当前呈现是否是第一次呈现(当组件被安装时)。它与其他一些答案大致相同,但是您可以在 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);
});
});

我们的用例是隐藏动画元素,如果最初的道具表明他们应该被隐藏。在稍后的渲染中,如果道具发生了变化,我们希望元素能够动画出来。

我找到了一个更简单的解决方案,不需要使用另一个钩子,但它有缺点。

useEffect(() => {
// skip initial render
return () => {
// do something with dependency
}
}, [dependency])

这只是一个例子,如果你的情况非常简单,还有其他方法可以做到这一点。

这样做的缺点是无法产生清理效果,只有当依赖项数组第二次更改时才会执行。

这是不推荐使用的,你应该使用其他答案所说的,但我只是在这里添加了这一点,以便人们知道有多种方法可以做到这一点。

编辑:

更明确地说,你不该用这个解决问题的方法(跳过最初的渲染) ,这只是为了教学的目的,表明你可以用不同的方式做同样的事情。 如果您需要跳过初始渲染,请使用其他答案的方法。

一个 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)
}

下面的解决方案类似于以上,只是一个小清洁的方式我更喜欢。

const [isMount, setIsMount] = useState(true);
useEffect(()=>{
if(isMount){
setIsMount(false);
return;
}
        

//Do anything here for 2nd render onwards
}, [args])

这里是我的实现基于 EstusFlask 的 回答写在类型。它也支持清理回调。

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);
}

现场演示

下面的现场演示展示了 useEffectuseDidUpdateEffect挂钩之间的区别

Edit 53179075/with-useeffect-how-can-i-skip-applying-an-effect-upon-the-initial-render

我本来想对目前已被接受的答案发表评论,但是没有空间了!

首先,在使用功能组件时,不要考虑生命周期事件,这一点很重要。考虑道具/状态的变化。我有一个类似的情况,我只希望一个特定的 useEffect函数触发时,一个特定的道具(在我的情况下,parentValue)从其初始状态变化。因此,我根据它的初始值创建了一个 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/

让我给你介绍一下 反应-使用

npm install react-use

想跑吗:

只有在第一次渲染之后

只有一次

检查它是第一个挂载 ? —— > useFirstMountState

想运行效果与 深刻的比较肤浅的比较油门? 和更多的 给你

不想安装一个库? 检查 密码和副本。(也许一个 star的好人也有)

最好是 又少了一件需要你维护的事。

可以使用 定制钩子在挂载后运行使用效果。

const useEffectAfterMount = (cb, dependencies) => {
const mounted = useRef(true);


useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};

以下是 typescript的版本:

const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
const mounted = useRef(true);


useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};

例如:

useEffectAfterMount(() => {
document.title = `You clicked ${count} times`;
}, [count])