React Hooks: 如果将一个空数组作为参数传递给useEffect(),它仍然会被调用两次。

608

我正在编写代码,以便在从数据库加载数据之前显示加载消息,然后在加载完成后使用加载的数据渲染组件。为了实现这一点,我同时使用了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;

为什么它被调用了两次,以及如何正确修复代码?

3
你说当你查看控制台日志(console.log),但是没有找到控制台日志。 - Joe Lloyd
3
起初我将它们删除了,因为我已经解释了发生了什么,但根据您的评论,我把它们重新加回来以增加清晰度。 - J.Ko
我的解决方案在 https://stackoverflow.com/a/72676006/2184182,我在这里分享。我猜可以给你帮助。 - Serkan KONAKCI
3
对于使用 React 18 的人 https://dev59.com/OlEG5IYBdhLWcg3wJlLh。翻译:针对React 18用户的问题,链接为https://dev59.com/OlEG5IYBdhLWcg3wJlLh。 - Youssouf Oumar
3
我相信这是因为<React.Strict>发生的。尝试将<React.Strict><Home></React.Strict>替换为只有<Home>。严格模式在本地渲染组件两次,在生产环境中渲染一次。 - Dhanesh Mane
23个回答

7
React严格模式在开发服务器上会将组件渲染两次。
因此,您需要从index.js中移除StrictMode。您的index.js当前代码可以如下所示。
root.render(
 <React.StrictMode>
 <App />
 </React.StrictMode>
);

移除StrictMode后,应该如下所示
root.render(
 <App />
);

5

我将这个作为我的备选方案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 () => {}
  }, [])

}

此外,还有一个情况是您在屏幕上进行了两次导航。


这不起作用。 - Harshan Morawaka

4

这是严格模式。在index.tsx或index.jsx中删除严格模式组件。


1
目前你的回答不够清晰,请编辑并添加更多细节,以帮助其他人理解它如何回答问题。你可以在帮助中心找到有关如何编写好答案的更多信息。 - Community

3
这可能不是最理想的解决方案,但我使用了一种变通方法。
var ranonce = false;
useEffect(() => {
    if (!ranonce) {

        //Run you code

        ranonce = true
    }
}, [])

尽管 useEffect 运行两次,但只有关键代码运行一次。

3

如果有人使用NextJS 13,在next.config.js文件中添加以下内容以删除Strict模式:

const nextConfig = {
  reactStrictMode: false
}
module.exports = nextConfig

当我创建项目时,默认使用了“严格模式”,因此我必须显式地设置它。


1
很酷,但是不在“严格模式”下运行应用程序的缺点是什么? - cy23
1
“严格模式”可以防止可能出现的问题,并给出一些警告。您可以在此处了解更多信息:https://react.dev/reference/react/StrictMode - Javi Villar

3

好的,也许现在评论有点晚了 - 但我找到了一个非常有用的解决方案,它是100%的React。

在我的情况下,我有一个令牌,我使用它来进行POST请求,以注销当前用户。

我正在使用一个带有以下状态的reducer:

export const INITIAL_STATE = {
  token: null
}

export const logoutReducer = (state, action) => {

  switch (action.type) {

    case ACTION_SET_TOKEN :

      state = {
        ...state,
        [action.name] : action.value
      };

      return state;

    default:
      throw new Error(`Invalid action: ${action}`);
  }

}

export const ACTION_SET_TOKEN = 0x1;

那么在我的组件中,我像这样检查状态:

import {useEffect, useReducer} from 'react';
import {INITIAL_STATE, ACTION_SET_TOKEN, logoutReducer} from "../reducers/logoutReducer";

const Logout = () => {

  const router = useRouter();
  const [state, dispatch] = useReducer(logoutReducer, INITIAL_STATE);

  useEffect(() => {

    if (!state.token) {
    
      let token = 'x' // .... get your token here, i'm using some event to get my token

      dispatch(
        {
          type : ACTION_SET_TOKEN,
          name : 'token',
          value : token
        }
      );
    
    } else {
    
      // make your POST request here
      
    }
    
 }

设计实际上很好 - 你有机会在POST请求后从存储中丢弃你的令牌,在任何操作之前确保POST成功。对于异步操作,你可以使用以下表单:

  POST().then(async() => {}).catch(async() => {}).finally(async() => {})

所有代码都在 useEffect 中运行——百分之百有效,我认为这正是 REACT 开发人员所想要的。这表明我实际上还有更多的清理工作要做(例如从存储中删除令牌等),在完成所有工作之前,现在我可以自由地导航到我的注销页面,而不会出现任何奇怪的情况。

以上是我的个人看法...


2

不确定为什么您不将结果放入状态中,这里有一个调用一次效果的示例,因此您必须在未发布的代码中执行某些操作,使其重新呈现:

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>


4
如果某人在使用useEffect钩子时遇到多个不需要的运行问题,我不建议使用类似useIsMounted这样的自定义钩子来增加额外的复杂性。他应该理解为什么会出现这种情况并相应地进行修复。 - Luis Gurmendez
@LuisGurmendez建议在使用异步结果设置状态之前先检查组件是否仍然挂载,这是一个明智的建议。如果操作存在意外调用效果,则问题中发布的代码并未证明该建议。 - HMR
如果是这种情况,他可以使用useEffect回调的返回函数来进行适当的清理。 - Luis Gurmendez
@LuisGurmendez 类方法 isMounted 与名为 useIsMounted 的自定义钩子毫无关系。只要以 use 开头,我可以将该钩子命名为任何名称。它们是完全不同的东西。 - HMR
回调函数的作用域中有一个对组件的引用。当“xhr promise”解析或拒绝时,它不会被清除,这不是清理的想法。想法是取消该请求,因此没有承诺的解决/拒绝。实际上有一些方法可以中止提取请求,请参阅此帖子https://dev59.com/U10Z5IYBdhLWcg3wyy11。 - Luis Gurmendez
显示剩余5条评论

1
我遇到过这样的问题,比如说:

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

这段文本的英译中文为:

会触发 useEffect 两次:


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

我确认该组件仅被挂载一次,并且setOnChainNFTs没有被多次调用 - 所以这不是问题的原因。

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

例如:

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

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

1
这是您需要的自定义钩子。它可能对您有帮助。
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);
}


0

没什么可担心的。当你在开发模式下运行React时,它有时会运行两次。在生产环境中进行测试,你的useEffect将只运行一次。不要担心!!


2
由于当前的回答写得不清楚,请[编辑]以添加更多详细信息,帮助其他人理解如何回答问题。您可以在帮助中心找到有关编写良好答案的更多信息。 - Community

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