当前位置: 首页 > 知识库问答 >
问题:

前端 - 怎么解决React useEffect 执行顺序导致设置 loading 状态错误的问题?

孔俊捷
2023-11-24

下面的代码,如果 count 连续发生两次变化,那么 loading 的状态就会被错误的提前设置为 false,怎么解决这个问题?

useEffect(() => {    console.log("set loading: true");    setLoading(true);    const ac = new AbortController();    new Promise<void>((resolve, reject) => {      const timer = setTimeout(() => {        console.log("resolve");        resolve();      }, 3000);      ac.signal.addEventListener("abort", () => {        clearTimeout(timer);        reject("abrted");      });    })      .then(() => {        setLazyCount(count);      })      .catch((err) => {        console.log("error:", err);      })      .finally(() => {        console.log("finally set loading: false");        setLoading(false);      });    return () => {      ac.abort();      console.log("abort");    };  }, [count]);

image.png


我尝试过把设置 loading 为true 的代码用 setTimeout 包裹起来,这样可以正确设置 loading,但是,当 Promise 立即完成的时候,顺序就又错误了。

useEffect(() => {    setTimeout(() => {        console.log("set loading: true");        setLoading(true);    });    const ac = new AbortController();    new Promise<void>((resolve, reject) => {      const timer = setTimeout(() => {        console.log("resolve");        resolve();// ...

然后把这个部分全部放在 settimeout 中才可以,但是这样写也太别扭了

useEffect(() => {    const ac = new AbortController();    setTimeout(() => {      console.log("set loading: true");      setLoading(true);      new Promise<void>((resolve, reject) => {        const timer = setTimeout(() => {          console.log("resolve");          resolve();        }, 3000);        ac.signal.addEventListener("abort", () => {          clearTimeout(timer);          reject("abrted");        });      })        .then(() => {          setLazyCount(count);        })        .catch((err) => {          console.log("error:", err);        })        .finally(() => {          console.log("finally set loading: false");          setLoading(false);        });    });    return () => {      ac.abort();      console.log("abort");    };  }, [count]);

在线测试 https://codesandbox.io/p/sandbox/use-effect-loading-fshhvm?fi...

共有3个答案

孔弘盛
2023-11-24
useEffect(() => {  let isActive = true;   console.log("set loading: true");  setLoading(true);  const ac = new AbortController();  new Promise<void>((resolve, reject) => {    const timer = setTimeout(() => {      console.log("resolve");      if (isActive) {        resolve();      }    }, 3000);    ac.signal.addEventListener("abort", () => {      clearTimeout(timer);      if (isActive) {        reject("aborted");      }    });  })    .then(() => {      if (isActive) {        setLazyCount(count);      }    })    .catch((err) => {      if (isActive) {        console.log("error:", err);      }    })    .finally(() => {      if (isActive) {        console.log("finally set loading: false");        setLoading(false);      }    });  return () => {    ac.abort();    isActive = false;     console.log("abort");  };}, [count]);
冀景明
2023-11-24

在你的代码逻辑里看起来不应该直接用true或者false来判断。
这边提供一个思路:
应该设置一个全局变量 LoadingNum初始化为0,大于0的时候加载loading
你的代码里设置为true的时候执行LoadingNum++
你的代码里设置为false的时候执行LoadingNum--
这样可以避免应前一次结束影响下一次loading加载

郎睿
2023-11-24

这个问题是由于 React useEffect 的执行顺序导致的。当 count 连续发生两次变化时,由于 useEffect 的回调函数是在组件重新渲染时才执行,因此 setLoading(true) 会在两次连续的渲染之间设置 loadingtrue。然后,当 Promise 立即完成时,setLoading(false) 会在 Promise 完成后立即执行,导致 loading 状态被错误地设置为 false

解决这个问题的一种方法是使用 useRef 钩子来存储上一个 Promise 是否已经完成的状态。在第一个 Promise 完成之后,将状态设置为 true,然后在第二个 Promise 完成之后,将状态设置为 false。这样就可以确保 loading 状态只在 Promise 完成之后被设置。

以下是修改后的代码:

import React, { useEffect, useRef, AbortController } from 'react';const App = () => {  const countRef = useRef(0);  const loadingRef = useRef(false);  const [count, setCount] = React.useState(0);  const [lazyCount, setLazyCount] = React.useState(0);  const [loading, setLoading] = React.useState(false);  useEffect(() => {    const ac = new AbortController();    const handleLoad = () => {      setLoading(true);      new Promise<void>((resolve, reject) => {        const timer = setTimeout(() => {          console.log("resolve");          resolve();        }, 3000);        ac.signal.addEventListener("abort", () => {          clearTimeout(timer);          reject("aborted");        });      })        .then(() => {          setLazyCount(count);        })        .catch((err) => {          console.log("error:", err);        })        .finally(() => {          console.log("finally set loading: false");          setLoading(false);        });    };    if (!loadingRef.current) {      loadingRef.current = true;      handleLoad();    } else {      clearTimeout(handleLoad); // 如果在上一个 Promise 完成之前又触发了一次 render,那么取消这次的 load    }    return () => ac.abort();  }, [count]);  return (    <div>      Count: {count}      <button onClick={() => setCount(count + 1)}>Increment</button>      {loading && <div>Loading...</div>}      {lazyCount && <div>Lazy Count: {lazyCount}</div>}    </div>  );};
 类似资料:
  • 1.手机翻转,或者折叠屏该怎么监听然后动态重新渲染echarts,需要加防抖或者节流吗? 2.横向条形图,左右两边的label怎么永远出现在可视范围内?比如148.00就已经飘出去了,看不全,还有左侧还有很多留白区域,不够美观 3.饼图中间的title的text和subtext能响应式的改变字体大小吗?像这种情况,字体完全超出了饼图范围

  • 如图, 第一个表格是我自己写的css换行导致的,使用show-overflow="false"就好了。 第二个表格即使取消换行,使用省略号也会有错位的问题,拉到最后就会错位,想问一下怎么解决,谢谢!

  • markdown图片可以传到本地文件夹但只能传jpg,png传不了,数据库也有路径,但是不渲染出来 头像上传七牛云,密钥和域名都写得对的但点击就报400

  • 如果dev分支里面部分功能要上生产,该怎么做?比如dev里有ACD这四个功能,A还在测试,生产里有CD这几个功能,然后目前要上线B功能,该怎么把B功能合到生产分支里? 首先不能把dev全部合到生产分支,也不能把开发B功能的分支合到生产分支吧?因为开发B功能的分支是由dev拉出来的,也会有A功能的代码。

  • 网址请求顺序是 souhu.com baidu.com tencent.com 这是什么原因呀?