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

340

根据文档:

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

我们可以使用新的useEffect()钩子来模拟componentDidUpdate(),但似乎useEffect()会在每次渲染时运行,甚至是第一次渲染。怎样才能使它不在初始渲染时运行?

如下面的示例所示,componentDidUpdateFunction在初始渲染时被打印,但componentDidUpdateClass没有在初始渲染时打印。

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

  return (
    <div>
      <p>componentDidUpdateFunction: {count} times</p>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Click Me
      </button>
    </div>
  );
}

class ComponentDidUpdateClass extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  componentDidUpdate() {
    console.log("componentDidUpdateClass");
  }

  render() {
    return (
      <div>
        <p>componentDidUpdateClass: {this.state.count} times</p>
        <button
          onClick={() => {
            this.setState({ count: this.state.count + 1 });
          }}
        >
          Click Me
        </button>
      </div>
    );
  }
}

ReactDOM.render(
  <div>
    <ComponentDidUpdateFunction />
    <ComponentDidUpdateClass />
  </div>,
  document.querySelector("#app")
);
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>


1
我可以问一下,在什么情况下根据渲染次数而不是显式状态变量(如count)来执行某些操作有意义? - Aprillion
@Aprillion,在我的情况下,需要更改一个H2的内容,该文本需要在项目列表之后更改,该列表为空并且一开始甚至不同。相同的列表在从API获取数据之前也为空,因此基于数组长度的正常条件呈现将覆盖初始值。 - Carmine Tambascia
19个回答

1
简化实现
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>
  )

}

1

@MehdiDehghani,你的解决方案完美地运行了,但还需要添加一点内容,在卸载时将didMount.current值重置为false。当你尝试在其他地方使用这个自定义hook时,你不会得到缓存值。

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;

3
如果组件最终卸载,我不确定这是否有必要,因为如果组件重新挂载,didMount 将已经被重新初始化为 false - Cameron Yick
@CameronYick 这是必要的,因为使用快速刷新时,清理函数将运行,但 ref 值不会被清除。这会导致在开发过程中出现故障 - 但在生产环境中则没有问题。 - andreialecu
我认为这会在每次依赖项更改时将 didMount.current 设置为 false,从而永远不会触发 effect!我错了吗?如果我们想要重置 didMount,那么它需要存在于一个 deps 为 [] 的单独的 useEffect 中。 - skot
是的,当我使用这段代码时,我看到的效果根本没有触发。 - skot

1
最近我需要在第一次渲染时运行第一个效果,因为我在其中初始化了一些存储,并且不希望应用程序在未执行此效果至少一次的情况下被渲染,所以我最终使用了一个新钩子 usePreffect :) 因为它可以在预渲染期间运行效果。
/**
 * Same as useEffect but runs the very first effect synchronously on the first render.
 * Useful for state initializing effects
 */
export const usePreffect = (effect: EffectCallback, deps: any[]): void => {
  const isFirstEffectRun = useRef(true);
  // Run the effect synchronously on the first render and save its cleanup function
  const firstCleanUp = useMemo(() => effect(), []);

  useEffect(() => {
    // Skip the first run and return the previously saved cleanup function
    if (isFirstEffectRun.current) {
      isFirstEffectRun.current = false;
      return firstCleanUp;
    }
    return effect();
  }, deps);
};

0

对于在React 18严格模式下遇到useEffect在初始渲染时被调用两次的人,可以尝试以下方法:

// The init variable is necessary if your state is an object/array, because the == operator compares the references, not the actual values.
const init = []; 
const [state, setState] = useState(init);
const dummyState = useRef(init);

useEffect(() => {
  // Compare the old state with the new state
  if (dummyState.current == state) {
    // This means that the component is mounting
  } else {
    // This means that the component updated.
    dummyState.current = state;
  }
}, [state]);

在开发模式下运行...

function App() {
  const init = []; 
  const [state, setState] = React.useState(init);
  const dummyState = React.useRef(init);

  React.useEffect(() => {
    if (dummyState.current == state) {
      console.log('mount');
    } else {
      console.log('update');
      dummyState.current = state;
    }
  }, [state]);
  
  return (
    <button onClick={() => setState([...state, Math.random()])}>Update state </button>
  );
}

ReactDOM.createRoot(document.getElementById("app")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>

<div id="app"></div>

而且在生产中。

function App() {
  const init = []; 
  const [state, setState] = React.useState(init);
  const dummyState = React.useRef(init);

  React.useEffect(() => {
    if (dummyState.current == state) {
      console.log('mount');
    } else {
      console.log('update');
      dummyState.current = state;
    }
  }, [state]);
  return (
    <button onClick={() => setState([...state, Math.random()])}>Update state </button>
    );
}

ReactDOM.createRoot(document.getElementById("app")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>

<div id="app"></div>


这是当你依赖于 [state] 并且它是正确的时候,但是当你需要相反的情况,使 React useEffect 仅在初始渲染 ([]) 上执行特定操作(例如设置一些标志)时,以下内容可能更合适:https://dev59.com/DVEG5IYBdhLWcg3wgPt3#76242764 - Hrvoje Golcic

0

让我们从根本的角度来看待 useEffect

请注意,以下解决方案利用了 useEffect 的基本特性,可能并不适用于所有需求。重要的是要记住,这个钩子函数在组件卸载时额外执行,在某些情况下可能是一个不错的权衡。

你所得到的是一个优雅而简洁的解决方案,没有任何其他人建议的额外复杂性。

简而言之

你可以使用清理函数来实现类似的结果。这个清理函数在每次正常的 effect 函数被调用之前*都会运行。

// a slick oneliner
useEffect(() => closeModal, [router.asPath])

// or 

useEffect(() => () => { // notice the two functions
  // do something here
}, [router.asPath])

// or

useEffect(() => {
  // not here
  return () => {
    // do something here
  }
}, [router.asPath])

为什么以及在什么时候这个工作会有效?

https://codesandbox.io/s/useeffect-lifecycle-s3gkzs

首先,为了理解useEffect生命周期的工作原理,让我们创建这个测试:
let i = 0

const MyComponent = () => {
  const router = useRouter() // Next.js router changes value when navigating

  console.log('hooktest: render', ++i)

  useEffect(() => {
    console.log('hooktest: setup', ++i)

    return () => {
      console.log('hooktest: cleanup', ++i)
    }
  }, [router.asPath])

  return null
}

渲染完这个组件后,我们会得到这个。
// first render (component is mounted)
hooktest: render 1
hooktest: setup 2
// navigated (2nd render)
hooktest: render 3
hooktest: cleanup 4
hooktest: setup 5
// navigated (3rd render)
hooktest: render 6
hooktest: cleanup 7
hooktest: setup 8
// app is closed (component gets unmounted)
hooktest: cleanup 9

有了这个知识,我们可以轻松地操作钩子,使得函数只在钩子第二次“执行”时运行。

*

React 何时清理 effect?
React 在组件卸载时进行清理。React 也会在下一次运行 effect 之前清理上一次渲染的 effect。
~ React 文档:
- [无清理的 effect](link1) - [有清理的 effect](link2)
注意:如果你在 中开发,你会看到 effect 运行两次。这只会在开发模式下发生。

显然,如果您确实需要清理功能,那么这个解决方案将不可行。但是,如果其他人建议引入额外的复杂性,那么这也是可以接受的。 - undefined

-1

之前的方法都不错,但是可以通过更简单的方式实现,考虑到在useEffect中的操作可以通过放置一个if条件(或其他条件)来“跳过”,这个条件基本上不会在第一次运行时执行,但仍然具有依赖性。

例如,我遇到了以下情况:

  1. 从API加载数据,但我的标题必须是“正在加载”,直到日期出现为止,因此我有一个数组tours,在开始时为空,并显示文本“显示”
  2. 使用与API不同的信息呈现组件。
  3. 用户可以逐个删除这些信息,甚至全部将tour数组再次清空,但这次API获取已经完成
  4. 一旦旅游列表通过删除变为空,则显示另一个标题。

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

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
@Luigi和你的if检查相同的变量并不完全相同,因此只会运行其中一个。在我的使用情况下,第一个if改变标题变量,第二个将状态变量设置回true。 - Carmine Tambascia
这是我的意思,顺序改变了。你先检查是否已经设置过它,如果是,就运行你的函数。我的函数首先检查我是否还没有设置它,然后再设置它。否则,它会运行我的函数(在你的情况下,是changeTitle)。 - Luigi
1
@Luigi 我曾经尝试过你的方法,但在我有这个用例时它并没有起作用,然后我想出了我上面写的内容,再次检查了两个不同的变量。目前我正在处理其他事情,但我会更好地解释为什么我必须检查两个不同的变量而不是相同的变量。此外,如果他们不清楚,就没有必要降低其他人的答案。我提供了一个几乎完整的示例,因此最终可能是您的增强,但这更像是一个边缘情况,否则我可以简单地使用其他答案,包括您的答案。 - Carmine Tambascia
我没有给它点踩。你的回答是有效的。 - Luigi

-1
如果您想跳过第一次渲染,可以创建一个名为“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]);

我使用了这个,我不明白有什么问题,如果有人能解释为什么会有负分吗? - Gishas
这个代码仅在第二次渲染时输出“componentDidUpdateFunction”,不会在任何后续渲染中输出。如果将firstRenderDone从deps数组中移除,它将正常工作。 - skot
1
但这种解决方案不如基于useRef的解决方案整洁,因为你的第一个渲染会立即触发第二个渲染。因此,你会立即获得一个componentDidUpdate事件,但实际上只有你正在更新的变量会被更新,以避免立即触发另一个更新。 - skot

-1
    const [dojob, setDojob] = useState(false);
yourfunction(){ setDojob(true); }
useEffect(()=>{ if(dojob){ yourfunction(); setDojob(false); } },[dojob]);
这段代码是关于编程的,它使用了React Hooks中的useState和useEffect函数。useState函数用于声明一个状态变量dojob,并将其初始值设置为false。yourfunction函数用于将dojob状态变量设置为true。useEffect函数则会在组件渲染后执行,当dojob状态变量发生改变时,会调用yourfunction函数并将dojob状态变量重置为false。

2
请参见“[答案]”和完全基于代码的解释。虽然这可能在技术上是正确的,但它并没有解释为什么它可以解决问题或应该被选为答案。我们应该在帮助解决问题的同时进行教育。 - the Tin Man
2
请不要发布仅包含代码的答案。未来的读者会感激您解释为什么这个答案回答了问题,而不是从代码中推断出来。此外,由于这是一个旧问题,请解释它如何补充所有其他答案。 - Gert Arnold

-5

大家都在给出复杂的答案,所以我就在这里放下:

这将在初始加载时呈现:

useEffect(() => {
    setData(data)
  });

这段代码不会在初始加载时呈现

useEffect(() => {
    setData(data)
  }, []);

只是一些关于Javascript的东西 :shrug:


1
这是不正确的。第一个将在每次渲染时运行,而第二个只会在初始渲染时运行一次。 - Ali Nauman
是的,第二个将在初始渲染时加载,但不会在初始加载时加载。因此,当您访问应用程序时,它不会获取数据,除非进行了一些更改,例如状态更改。 - knaitas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接