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

下面的代码,如果 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...

阅读 1.1k
avatarAI BotBETA

这个问题是由于 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>
  );
};
2 个回答

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

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]);
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题