React Hooks:即使使用空数组作为参数,也会调用两次 useEffect()

Posted

技术标签:

【中文标题】React Hooks:即使使用空数组作为参数,也会调用两次 useEffect()【英文标题】:React Hooks: useEffect() is called twice even if an empty array is used as an argument 【发布时间】:2020-06-22 10:08:32 【问题描述】:

我是 reactJS 的新手并且正在编写代码,以便在从 DB 加载数据之前,它会显示加载消息,然后在加载之后,使用加载的数据渲染组件。为此,我同时使用了 useState 钩子和 useEffect 钩子。代码如下:

问题是,当我检查 console.log 时,useEffect 被触发了两次。因此,代码会两次查询相同的数据,应该避免这种情况。

下面是我写的代码:

import React from 'react';
import './App.css';
import useState,useEffect from 'react';
import Postspreview from '../components/Postspreview'

const indexarray=[]; //The array to which the fetched data will be pushed

function Home() 
   const [isLoading,setLoad]=useState(true);
   useEffect(()=>
      /*
      Query logic to query from DB and push to indexarray
      */
          setLoad(false);  // To indicate that the loading is complete
    )
   ,[]);
   if (isLoading===true)
       console.log("Loading");
       return <div>This is loading...</div>
   
   else 
       console.log("Loaded!"); //This is actually logged twice.
       return (
          <div>
             <div className="posts_preview_columns">
             indexarray.map(indexarray=>
             <Postspreview
                username=indexarray.username
                idThumbnail=indexarray.profile_thumbnail
                nickname=indexarray.nickname
                postThumbnail=indexarray.photolink
             />
             )
            </div>
         </div>  
         );
    


export default Home;

有人可以帮助我理解为什么它被调用两次,以及如何正确修复代码吗? 非常感谢!

【问题讨论】:

你说当你检查console.log但没有console.log时 最初删除了它们,因为我几乎解释了发生的事情,但为了清楚起见,根据您的评论将它们添加回来。 【参考方案1】:

把console.log放到useEffect里面

您可能还有其他副作用导致组件重新呈现,但 useEffect 本身只会被调用一次。您可以通过以下代码确定地看到这一点。

useEffect(()=>
      /*
      Query logic
      */
      console.log('i fire once');
,[]);

如果日志“i fire once”被多次触发,则意味着您的问题是 两件事之一。

此组件在您的页面中出现多次

这个应该很明显,你的组件在页面中出现了几次,每次都会挂载并运行 useEffect

树上更高的东西正在卸载和重新安装

组件在其初始渲染时被强制卸载并重新安装。这可能类似于在树的更高处发生的“关键”变化。您需要使用此 useEffect 升级每个级别,直到它只渲染一次。那么您应该能够找到原因或重新安装。

React.Strict 模式开启

StrictMode 渲染组件两次(在开发中而不是在生产中),以便检测代码中的任何问题并向您发出警告(这可能非常有用)。

此答案由@johnhendirx 指出并由@rangfu 撰写,请参阅link,如果这是您的问题,请给他一些爱。

【讨论】:

还有一个注意事项:仔细检查您是否在严格模式下使用 React。如果是这样,请参阅此处:***.com/questions/61254372/… 就我而言,它是卸载和安装。我对 props.searchResult 有一个有效的依赖项,我在搜索完成后显示结果,但显示结果组件的条件是“!isSearching && searchDone”,当我再次单击搜索时,isSearching 设置为 true(卸载)然后返回为假(安装),但旧结果仍处于状态,然后我设置了新结果 - 所以效果被调用了两次。为了解决这个问题,我必须先设置结果,然后将 isSearching 设置为 false。谢谢!我不容易注意到这一点。【参考方案2】:

您很可能在启用严格模式的开发环境中检查问题。 要验证是否是这种情况,请搜索 标记并将其删除,或者构建用于生产。双重渲染问题应该消失了。 来自 React 官方文档

严格模式无法自动为您检测副作用,但可以通过使它们更具确定性来帮助您发现它们。这是通过有意双重调用以下函数来完成的:

传递给 useState、useMemo 或 useReducer 的函数 [...]

Strict Mode - Reactjs docs

这里有类似的问题My React Component is rendering twice because of Strict Mode

【讨论】:

这是我的问题。谢谢!【参考方案3】:

我使用它作为我的替代useFocusEffect。我使用了嵌套的 react 导航堆栈,例如选项卡和抽屉,使用 useEffect 进行重构并没有按预期对我起作用。

import React,  useEffect, useState  from 'react'
import  useFocusEffect  from '@react-navigation/native'

const app = () = 

  const [isloaded, setLoaded] = useState(false)


  useFocusEffect(() => 
      if (!isloaded) 
        console.log('This should called once')

        setLoaded(true)
      
    return () => 
  , [])


另外,还有一个你在屏幕上导航两次的实例。

【讨论】:

【参考方案4】:

不确定为什么不将结果置于状态中,这里有一个调用效果的示例,因此您必须在未发布的代码中做了一些使其再次呈现的事情:

const App = () => 
  const [isLoading, setLoad] = React.useState(true)
  const [data, setData] = React.useState([])
  React.useEffect(() => 
    console.log('in effect')
    fetch('https://jsonplaceholder.typicode.com/todos')
      .then(result => result.json())
      .then(data => 
        setLoad(false)//causes re render
        setData(data)//causes re render
      )
  ,[])
  //first log in console, effect happens after render
  console.log('rendering:', data.length, isLoading)
  return <pre>JSON.stringify(data, undefined, 2)</pre>


//render app
ReactDOM.render(<App />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

为了防止额外的渲染,您可以在一种状态下合并数据和加载:

const useIsMounted = () => 
  const isMounted = React.useRef(false);
  React.useEffect(() => 
    isMounted.current = true;
    return () => isMounted.current = false;
  , []);
  return isMounted;
;


const App = () => 
  const [result, setResult] = React.useState(
    loading: true,
    data: []
  )
  const isMounted = useIsMounted();
  React.useEffect(() => 
    console.log('in effect')
    fetch('https://jsonplaceholder.typicode.com/todos')
      .then(result => result.json())
      .then(data => 
        //before setting state in async function you should
        //  alsways check if the component is still mounted or
        //  react will spit out warnings
        isMounted.current && setResult( loading: false, data )
      )
  ,[isMounted])
  console.log(
    'rendering:',
    result.data.length,
    result.loading
  )
  return (
    <pre>JSON.stringify(result.data, undefined, 2)</pre>
  )


//render app
ReactDOM.render(<App />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

【讨论】:

对于遇到多次不希望运行的 useEffect 钩子问题的人,我不建议使用这样的自定义钩子(useIsMounted)增加额外的复杂性。他应该理解为什么会发生这种情况并修复它相应地。 @LuisGurmendez 建议在异步结果设置状态之前检查组件是否仍然挂载是合理的建议。如果 op 对效果进行了意外调用,则问题中发布的代码并未证明这一点。 如果是这种情况,他可以使用 useEffect 回调的返回函数进行适当的清理 es.reactjs.org/blog/2015/12/16/ismounted-antipattern.html @LuisGurmendez 类方法isMounted 与名为useIsMounted 的自定义钩子无关,只要它以use 开头,我就可以调用任何钩子。它们是完全不同的东西。【参考方案5】:

这是用于您的目的的自定义挂钩。它可能对您的情况有所帮助。

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

/**
 * 
 * @param effect 
 * @param dependencies
 * @description Hook to prevent running the useEffect on the first render
 *  
 */
export default function useNoInitialEffect(
  effect: EffectCallback,
  dependancies?: 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;
  , dependancies);


【讨论】:

【参考方案6】:

我遇到过类似的问题:

const [onChainNFTs, setOnChainNFTs] = useState([]);

会触发这个 useEffect 两次:

useEffect(() => 
    console.log('do something as initial state of onChainNFTs changed'); // triggered 2 times
, [onChainNFTs]);

我确认组件 MOUNTED ONLY ONCE 和 setOnChainNFTs 没有被多次调用 - 所以这不是问题。

我通过将 onChainNFTs 的初始状态转换为 null 并进行空值检查来修复它。

例如

const [onChainNFTs, setOnChainNFTs] = useState(null);
useEffect(() => 
if (onChainNFTs !== null) 
    console.log('do something as initial state of onChainNFTs changed'); // triggered 1 time!

, [onChainNFTs]);

【讨论】:

以上是关于React Hooks:即使使用空数组作为参数,也会调用两次 useEffect()的主要内容,如果未能解决你的问题,请参考以下文章

React使用hooks-useMemo

react之Hook的useEffect详解

React Hooks - 我如何实现 shouldComponentUpdate?

React Hooks:确保 useEffect 仅在自定义钩子的数组参数内容更改时运行的惯用方法

使用 React Hooks 获取帖子并将它们作为附加道具传递给另一个组件

React Hooks:如何在 useEffect 中设置状态?