ChatGPT解决这个技术问题 Extra ChatGPT

使 React useEffect 钩子不在初始渲染时运行

根据文档:

更新发生后立即调用 componentDidUpdate()。初始渲染不调用此方法。

我们可以使用新的 useEffect() 挂钩来模拟 componentDidUpdate(),但似乎 useEffect() 是在每次渲染之后运行的,即使是第一次。如何让它不在初始渲染时运行?

正如您在下面的示例中所见,在初始渲染期间打印了 componentDidUpdateFunction,但在初始渲染期间未打印 componentDidUpdateClass

函数 ComponentDidUpdateFunction() { const [count, setCount] = React.useState(0); React.useEffect(() => { console.log("componentDidUpdateFunction"); }); return (

componentDidUpdateFunction: {count} 次

); } 类 ComponentDidUpdateClass 扩展 React.Component { 构造函数(道具) { 超级(道具); this.state = { count: 0, }; } componentDidUpdate() { console.log("componentDidUpdateClass"); } render() { return (

componentDidUpdateClass: {this.state.count} 次

); } } ReactDOM.render(
, document.querySelector("#app") );

我可以问一下,当基于渲染次数而不是像 count 这样的显式状态变量做某事有意义时,用例是什么?
@Aprillion,在我的情况下,更改 H2 的内容,该内容具有需要在项目列表之后更改的文本,是空的,并且在开始时甚至不同。在从 API 获取数据之前,相同的列表在开始时也是空的,因此对于基于数组长度的正常条件渲染,初始值会被覆盖

T
Tholle

We can use the useRef hook to store any mutable value we like,因此我们可以使用它来跟踪是否是第一次运行 useEffect 函数。

如果我们希望效果与 componentDidUpdate 在同一阶段运行,我们可以使用 useLayoutEffect 代替。

例子

常量 { useState, useRef, useLayoutEffect } = React;函数 ComponentDidUpdateFunction() { const [count, setCount] = useState(0);常量 firstUpdate = useRef(true); useLayoutEffect(() => { if (firstUpdate.current) { firstUpdate.current = false; return; } console.log("componentDidUpdateFunction"); }); return (

componentDidUpdateFunction: {count} 次

); } ReactDOM.render( , document.getElementById("app") );


我试图用 useState 替换 useRef,但使用 setter 触发了重新渲染,分配给 firstUpdate.current 时没有发生这种情况,所以我想这是唯一的好方法 :)
如果我们不改变或测量 DOM,有人可以解释为什么要使用布局效果吗?
@ZenVentzi 在这个例子中没有必要,但问题是如何用钩子模仿 componentDidUpdate,所以这就是我使用它的原因。
我根据这个答案创建了一个自定义挂钩 here。感谢您的实施!
M
Mehdi Dehghani

您可以将其变成 custom hooks,如下所示:

import React, { useEffect, useRef } from 'react';

const useDidMountEffect = (func, deps) => {
    const didMount = useRef(false);

    useEffect(() => {
        if (didMount.current) func();
        else didMount.current = true;
    }, deps);
}

export default useDidMountEffect;

使用示例:

import React, { useState, useEffect } from 'react';

import useDidMountEffect from '../path/to/useDidMountEffect';

const MyComponent = (props) => {    
    const [state, setState] = useState({
        key: false
    });    

    useEffect(() => {
        // you know what is this, don't you?
    }, []);

    useDidMountEffect(() => {
        // react please run me if 'key' changes, but not on initial render
    }, [state.key]);    

    return (
        <div>
             ...
        </div>
    );
}
// ...

这种方法会引发警告说依赖列表不是数组文字。
我在我的项目中使用了这个钩子,我没有看到任何警告,你能提供更多信息吗?
@vsync您正在考虑另一种情况,您希望在初始渲染时运行一次效果,然后再也不运行
@vsync 在 reactjs.org/docs/… 的注释部分中,它特别指出“如果您想运行一个效果并只清理一次(在挂载和卸载时),您可以传递一个空数组 ([]) 作为第二个参数。”这符合我观察到的行为。
我还收到依赖列表不是数组文字和缺少依赖项的警告:'func'。两者的 linter 规则是 react-hooks/exhaustive-deps
M
Marius Marais

我做了一个简单的 useFirstRender 钩子来处理像关注表单输入这样的情况:

import { useRef, useEffect } from 'react';

export function useFirstRender() {
  const firstRender = useRef(true);

  useEffect(() => {
    firstRender.current = false;
  }, []);

  return firstRender.current;
}

它以 true 开始,然后切换到 useEffect 中的 false,它只运行一次,并且不再运行。

在您的组件中,使用它:

const firstRender = useFirstRender();
const phoneNumberRef = useRef(null);

useEffect(() => {
  if (firstRender || errors.phoneNumber) {
    phoneNumberRef.current.focus();
  }
}, [firstRender, errors.phoneNumber]);

对于您的情况,您只需使用 if (!firstRender) { ...


如果将 firstRender 添加到 useEffect 中的依赖项数组中,这将在挂载时运行两次(第一次,当 firstRender 设置为 false 时)。我从我的代码中的依赖项中删除了它并且它有效。
@RafaelDuarte 我认为不会。据我所知,React 不会在 Ref 更新时触发重新渲染。如果 firstRender 是一个状态,它会做。我错了吗?编辑:哦,但是当钩子结果发生变化时,它可能会重新渲染......
从内存中,它将重新渲染。这就是 if 的用途 :)
L
Luigi

方法与 Tholle's answer 相同,但使用 useState 而不是 useRef

const [skipCount, setSkipCount] = useState(true);

...

useEffect(() => {
    if (skipCount) setSkipCount(false);
    if (!skipCount) runYourFunction();
}, [dependencies])

编辑

虽然这也有效,但它涉及更新状态,这将导致您的组件重新渲染。如果您的所有组件的 useEffect 调用(以及它的所有子组件)都具有依赖关系数组,那么这无关紧要。但请记住,任何没有依赖关系数组的 useEffect (useEffect(() => {...}) 都将再次运行。

使用和更新 useRef 不会导致任何重新呈现。


这是一个很好的解决方案
W
Whatabrain

@ravi,你的不调用传入的卸载函数。这是一个更完整的版本:

/** * 与 React.useEffect 相同,只是它永远不会在挂载时运行。这是 * componentDidUpdate 生命周期函数的等价物。 * * @param {function:function} 效果 - 一个 useEffect 效果。 * @param {array} [dependencies] - useEffect 依赖列表。 */ export const useEffectExceptOnMount = (effect, dependencies) => { const Mounted = React.useRef(false); React.useEffect(() => { if (mounted.current) { const unmount = effect(); return () => unmount && unmount(); } else {mounted.current = true; } }, 依赖项); // 在卸载时重置以进行下一次装载。 React.useEffect(() => { return () =>mounted.current = false; }, []); };


你好@Whatabrain,如何在传递非依赖列表时使用这个自定义钩子?不是空的,它与 componentDidmount 相同,但类似于 useEffect(() => {...});
@KevDing 应该像在调用它时省略 dependencies 参数一样简单。
您可能会丢失第二个 useEffect。它什么也没做。您正在重置的那个 ref 直接进入垃圾箱。如果有一个“下一个挂载”,它将发生在另一个具有自己的 mounted 引用的钩子实例上,初始化为 false。
const unmount = effect(); return () => unmount && unmount(); 可以简化为 return effect()
@skot 我写了第二个效果是为了完整性,但是在下一个版本的 React 中,多个挂载和卸载将是可能的,这使得它更加必要。很好地理解了卸载简化。你是对的!
D
Dominik

这是迄今为止我使用 typescript 创建的最佳实现。基本上,这个想法是相同的,使用 Ref 但我也在考虑 useEffect 返回的回调来对组件卸载执行清理。

import {
  useRef,
  EffectCallback,
  DependencyList,
  useEffect
} from 'react';

/**
 * @param effect 
 * @param dependencies
 *  
 */
export default function useNoInitialEffect(
  effect: EffectCallback,
  dependencies?: DependencyList
) {
  //Preserving the true by default as initial render cycle
  const initialRender = useRef(true);

  useEffect(() => {
    let effectReturns: void | (() => void) = () => {};

    // Updating the ref to false on the first render, causing
    // subsequent render to execute the effect
    if (initialRender.current) {
      initialRender.current = false;
    } else {
      effectReturns = effect();
    }

    // Preserving and allowing the Destructor returned by the effect
    // to execute on component unmount and perform cleanup if
    // required.
    if (effectReturns && typeof effectReturns === 'function') {
      return effectReturns;
    } 
    return undefined;
  }, dependencies);
}

您可以像使用 useEffect 挂钩一样简单地使用它,但是这一次,它不会在初始渲染时运行。以下是如何使用这个钩子。

useuseNoInitialEffect(() => {
  // perform something, returning callback is supported
}, [a, b]);

如果您使用 ESLint 并希望对此自定义挂钩使用 react-hooks/exhaustive-deps 规则:

{
  "rules": {
    // ...
    "react-hooks/exhaustive-deps": ["warn", {
      "additionalHooks": "useNoInitialEffect"
    }]
  }
}

你需要围绕返回值的所有逻辑吗?你不能只return effect()吗?
eslint 规则是金
s
skot

把事情简单化:

function useEffectAfterFirstRender(effect, deps) {
  const isFirstRender = useRef(true);

  useEffect(() => {
    if (isFirstRender.current) isFirstRender.current = false;
    else return effect();
  }, deps);
}

如果您消除不必要的并发症,这里的其他解决方案会减少这种情况:

我们需要传递 effect() 的返回值,因为它可能是一个析构函数,但我们不需要做任何条件逻辑来确定它是否存在。只需传递它,不管它是什么,让 useEffect 弄清楚。

在卸载时将 isFirstRender 重置为 true 是没有意义的,因为 1)条件尚未变为真,并且 2)在卸载时,该 ref 将进入焚化炉。它不会在“下一个安装”上重复使用。没有下一个坐骑。卸载就是死亡。

这是一个完整的打字稿模块:

import { useEffect, useRef, EffectCallback, DependencyList } from 'react';

function useEffectAfterFirstRender(effect: EffectCallback, deps: DependencyList): void {
  const isFirstRender = useRef(true);

  useEffect(() => {
    if (isFirstRender.current) isFirstRender.current = false;
    else return effect();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, deps);
}

export default useEffectAfterFirstRender;

我赞成 Kiran Maniya 的建议,给它一个详尽的 deps eslint 规则:

{
  "rules": {
    "react-hooks/exhaustive-deps": ["warn", {
      "additionalHooks": "useEffectAfterFirstRender"
    }]
  }
}

s
scripty dude

一种简单的方法是从您的组件中创建一个 let 并将其设置为 true。

然后说如果它的真设置为假然后返回(停止)useEffect函数

像那样:


    import { useEffect} from 'react';
    //your let must be out of component to avoid re-evaluation 
    
    let isFirst = true
    
    function App() {
      useEffect(() => {
          if(isFirst){
            isFirst = false
            return
          }
    
        //your code that don't want to execute at first time
      },[])
      return (
        <div>
            <p>its simple huh...</p>
        </div>
      );
    }

它类似于@Carmine Tambasciabs 解决方案,但不使用状态:) ‍‍‍‍‍‍ ‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍


W
Whatabrain

@MehdiDehghani,您的解决方案工作得非常好,您必须做的一个补充是在卸载时,将 didMount.current 值重置为 false。何时尝试在其他地方使用此自定义挂钩,您不会获得缓存值。

import React, { useEffect, useRef } from 'react';

const useDidMountEffect = (func, deps) => {
    const didMount = useRef(false);

    useEffect(() => {
        let unmount;
        if (didMount.current) unmount = func();
        else didMount.current = true;

        return () => {
            didMount.current = false;
            unmount && unmount();
        }
    }, deps);
}

export default useDidMountEffect;

我不确定这是否必要,因为如果组件最终卸载,因为如果它重新安装,didMount 将已经重新初始化为 false
@CameronYick 这是必要的,因为使用快速刷新时,清理功能将运行,但 ref 值不会被清除。这会导致开发时出现故障 - 但在生产中会很好。
我认为这会在每次 deps 更改时将 didMount.current 设置为 false,因此永远不会触发效果!我错了吗?如果我们需要,该 didMount 重置将需要位于一个单独的 useEffect 中,其 deps 是 []
是的,当我使用这段代码时,我看到的是效果根本不会触发。
s
shahul01

简化实施

import { useRef, useEffect } from 'react';

function MyComp(props) {

  const firstRender = useRef(true);

  useEffect(() => {
    if (firstRender.current) {
      firstRender.current = false;
    } else {
      myProp = 'some val';
    };

  }, [props.myProp])


  return (
    <div>
      ...
    </div>
  )

}

A
A__

我认为创建一个自定义钩子会过大,我不想通过使用 useLayoutEffect 钩子来处理与布局无关的东西来混淆我的组件的可读性,所以,就我而言,我只是检查了我的 stateful 的值触发 useEffect 回调的变量 selectedItem 是它的原始值,以确定它是否是初始渲染:

export default function MyComponent(props) {
    const [selectedItem, setSelectedItem] = useState(null);

    useEffect(() => {
        if(!selectedItem) return; // If selected item is its initial value (null), don't continue
        
        //... This will not happen on initial render

    }, [selectedItem]);

    // ...

}

如果这个特定的状态变量在整个组件生命周期的某个时刻再次更改为这个值怎么办?这是一个有人知道 100% 永远不会发生的情况,因为作为 useEffect 依赖,它的目的是控制状态变化的副作用。
如果您要问“如果 selectedItem 再次变为 null 怎么办”,那么是的,您需要 A)确保它永远不会再次设置为 null,或者 B)使用除此之外的值初始化 useState() null,您知道它永远不会被再次设置(即 -1)。
L
Leandro Ortiz

如果你想跳过第一次渲染,你可以创建一个状态“firstRenderDone”,并在 useEffect 中将其设置为 true,并且依赖列表为空(类似于 didMount)。然后,在你的另一个 useEffect 中,你可以在做某事之前检查第一次渲染是否已经完成。

const [firstRenderDone, setFirstRenderDone] = useState(false);

//useEffect with empty dependecy list (that works like a componentDidMount)
useEffect(() => {
  setFirstRenderDone(true);
}, []);

// your other useEffect (that works as componetDidUpdate)
useEffect(() => {
  if(firstRenderDone){
    console.log("componentDidUpdateFunction");
  }
}, [firstRenderDone]);

我用过这个,我不明白这有什么问题,如果有人可以解释为什么会有负面影响?
这仅在第二次渲染时输出“componentDidUpdateFunction”,而不是在任何后续渲染中。如果您将 firstRenderDone 从 deps 数组中取出,这将起作用。
但它不像基于 useRef 的解决方案那样整洁,因为您的第一个渲染会立即触发第二个渲染。因此,您会立即收到一个 componentDidUpdate 事件,但实际更新的只是您用来避免立即触发更新的变量。
C
Carmine Tambascia

之前的一切都很好,但是考虑到 useEffect 中的操作可以“跳过”放置一个基本上不是第一次运行并且仍然具有依赖关系的 if 条件(或任何其他),这可以通过更简单的方式实现。

例如,我有以下情况:

从 API 加载数据,但我的标题必须是“正在加载”,直到日期不存在,所以我有一个数组,游览开始时为空并显示文本“正在显示”有一个组件使用来自这些 API 的不同信息呈现.用户可以一个一个地删除这些信息,甚至所有这些都使游览数组再次为空,但这次API获取已经完成一旦游览列表为空,通过删除然后显示另一个标题。

所以我的“解决方案”是创建另一个 useState 来创建一个布尔值,该值仅在数据获取后才更改,使 useEffect 中的另一个条件为 true,以便运行另一个也取决于游览长度的函数。

useEffect(() => {
  if (isTitle) {
    changeTitle(newTitle)
  }else{
    isSetTitle(true)
  }
}, [tours])

这里是我的 App.js

import React, { useState, useEffect } from 'react'
import Loading from './Loading'
import Tours from './Tours'

const url = 'API url'

let newTours

function App() {
  const [loading, setLoading ] = useState(true)
  const [tours, setTours] = useState([])
  const [isTitle, isSetTitle] = useState(false)
  const [title, setTitle] = useState("Our Tours")

  const newTitle = "Tours are empty"

  const removeTours = (id) => {
    newTours = tours.filter(tour => ( tour.id !== id))

    return setTours(newTours)
  }

  const changeTitle = (title) =>{
    if(tours.length === 0 && loading === false){
      setTitle(title)
    }
  }

const fetchTours = async () => {
  setLoading(true)

  try {
    const response = await fetch(url)
    const tours = await response.json()
    setLoading(false)
    setTours(tours)
  }catch(error) {
    setLoading(false)
    console.log(error)
  }  
}


useEffect(()=>{
  fetchTours()
},[])

useEffect(() => {
  if (isTitle) {
    changeTitle(newTitle)
  }else{
    isSetTitle(true)
  }
}, [tours])


if(loading){
  return (
    <main>
      <Loading />
    </main>
  )  
}else{
  return ( 

    <main>
      <Tours tours={tours} title={title} changeTitle={changeTitle}           
removeTours={removeTours} />
    </main>
  )  
 }
}



export default App

除了 if 语句上的条件顺序之外,我认为这种方法没有区别。
@Luigi 与您的 if 检查相同的变量并不完全相同,因此运行一个或另一个,在我的用例中,首先如果更改标题变量,第二个将状态变量设置回 true
这就是我改变订单的意思。你检查你之前是否设置过它,如果有,你运行你的函数。我的首先检查我是否还没有设置它,然后设置它。否则,它运行我的函数(在你的情况下,changeTitle)。
@Luigi一旦我有这个用例并且没有工作,我已经尝试过你的方法,然后我想出了我上面写的内容,然后我再次检查了2个不同的变量。目前我已经完成了其他工作,但我会更好地解释为什么我必须检查 2 个不同的变量,而不是同一个变量。如果其他人回答不清楚,也不需要降级。我提供了一个几乎完整的示例,因此最终可能会增强您的示例,但更多的是边缘情况,否则我本可以简单地使用其他答案,包括您的答案。
我没有投反对票。你的答案有效。