简体   繁体   中英

Must I specify unique Loading booleans when fetching data via multiple async functions within useEffects in React hooks to avoid concurrency problems?

My concern is if using the same isLoading boolean state can cause concurrency issues. Here's an example of what I'm talking about:

  const [isLoading, setIsLoading] = useState<boolean>(false);


  useEffect(() => {
    async function getWaitlistDetailsOnLoad() {
      setIsLoading(true)
      try {
        const details = await getWaitlistDetailsForUser(
          apiInstance,
          product.id
        );
        if (details.status === 200) {
          setWaitlistDetails(details.data);
        }
      } catch (e) {
        console.log("Error getting waitlist details: ", e);
      }
      setIsLoading(false)
    }
    getWaitlistDetailsOnLoad();
  }, [apiInstance, product.id]);

  useEffect(() => {
    async function getNextAvailableTimeOnLoad() {
      setIsLoading(true)
      try {
        const time = await getNextAvailableTime(apiInstance, product.id);
        if (time.status === 200) {
          setNextAvailableTime(time.data);
        }
        setIsLoading(false);
      } catch (e) {
        console.log("Error getting next available time: ", e);
      }
      setIsLoading(false)
    }
    getNextAvailableTimeOnLoad();
  }, [apiInstance, product.id]);

Of course I can just track two independent loading states like this:

  const [firstLoader, setFirstLoader] = useState<boolean>(false);
  const [secondLoader, setSecondLoader] = useState<boolean>(false);

...but if I don't have to, I'd rather not. It would make the code simpler and conditional rendering simpler as well for my use-case.

As @alexander-nied already pointed out there might by a bug in your code. useEffect callbacks can't be async. To use async/await nontheless you can wrap the three lines as follows and add an await statement.

(async () => {
    setIsLoading(true);
    await getNextAvailableTimeOnLoad();
    setIsLoading(false);
})();

To answer your question: It certainly depends on you usecase, but I would recommend against just using one single boolean, since it allows for weired scenarios to occur.

What if one request is way faster than the other? Your loading indicator would be set to false, instead of informing the user that there is still work being done in the background.

If you are guaranteed that the two asynchronous effects will not run at the same time (for instance, if OperationTwo , if one is called at the completion of OperationOne ) then you could get away with using a single isLoading boolean, set as true at the start of OperationOne , and set as false at the completion of OperationTwo .

However, if you have two operations that might at any point run at the same time, then you should split them into two separate loaders, and use a single value that OR s them to determine the ultimate loading state of the view.

Let's consider a component that makes two asynchronous fetches on load:

const MyPretendComponent = () => {
    const [isLoading, setIsLoading] = useState(false);

    useEffect(() => {
      setIsLoading(true);
      MyService.fetchThatCompletesInOneSecond()
        .then(() => setIsLoading(false));
      MyService.fetchThatCompletesInTwentySeconds()
        .then(() => setIsLoading(false));
    }, [])
    
    return (<h1>{`isLoading ? "Loading" : "Finished!"`}</h1>);
}

This should illustrate the problem nicely-- we have two asynchronous operations on component load-- one that completes in one second, and one that completes in twenty seconds. Both set isLoading as false when they complete. In this case, the first operation completes in one second and sets isLoading to false , and the UI erroneously reports that it is not in a loading state even though the second operation still has nineteen seconds left until it completes.

The cleaner version, using two booleans, is this:

 const MyPretendComponent = () => { const [isFirstOperationLoading, setIsFirstOperationLoading] = useState(false); const [isSecondOperationLoading, setIsSecondOperationLoading] = useState(false); useEffect(() => { setIsFirstOperationLoading(true); setIsSecondOperationLoading(true) MyService.fetchThatCompletesInOneSecond().then(() => setIsFirstOperationLoading(false)); MyService.fetchThatCompletesInTwentySeconds().then(() => setIsSecondOperationLoading(false)); }, []) const isLoading = isFirstOperationLoading || isSecondOperationLoading; return (<h1>{`isLoading? "Loading": "Finished;"`}</h1>); }

Here we've split the two loading states into their own discrete booleans. We still maintain a single isLoading boolean that simply OR s both the discrete booleans to determine if the overall loading state of the component is loading or not loading. Note that, in a real-life example, we would want to use much more semantically descriptive variable names than isFirstOperationLoading and isSecondOperationLoading .

In regards to splitting the two calls into separate useEffect s: at a glance one might think that in splitting the two calls across two different useEffect s you could mitigate or sidestep the issue of asynchronicity. However, if we walk through the component lifecycle, and think we will learn that this is not actually effective. Let's update our first example:

const MyPretendComponent = () => {
    const [isFirstOperationLoading, setIsFirstOperationLoading] = useState(false);
    const [isSecondOperationLoading, setIsSecondOperationLoading] = useState(false);

    useEffect(() => {
      setIsFirstOperationLoading(true);
      setIsSecondOperationLoading(true)
      MyService.fetchThatCompletesInOneSecond()
        .then(() => setIsFirstOperationLoading(false));
      MyService.fetchThatCompletesInTwentySeconds()
        .then(() => setIsSecondOperationLoading(false));
    }, [])
    
    const isLoading = isFirstOperationLoading || isSecondOperationLoading;
    
    return (<h1>{`isLoading ? "Loading" : "Finished!"`}</h1>);
}

The problem here is that the component does not run in this manner:

  1. Run the first useEffect
  2. When the first useEffect completes, run the second useEffect
  3. When the second useEffect completes, render the UI

If it ran in that manner, we would be sitting and waiting for it to show the UI while it loaded.

Instead, the way it runs is like this:

  1. Run the component render-- call any useEffect s if applicable, then render the UI
  2. Should any state updates occur (for instance, a state hook being called from the .then of a previous useEffect asynchronous call) then render again
  3. Repeat step two every time a state or prop update occurs

Keeping this in mind, at every component render React will evaluate and run any and all applicable useEffect callbacks. It is not running them serially and tracking if one has completed-- such a thing would be difficult if not impossible. Instead, it is the developer's responsibility to track loading states and organize useEffect dependency arrays in such a way that your component state is logical and consistent.

So you see, separating the async calls by useEffect does not save us from dealing with potential concurrent async calls. In fact, if it did, it would mean that the UI load would be slower overall, because you would be serially calling two calls that could be called simultaneously. For instance, if both async calls took ten seconds each, calling them one after the other would mean it would take 20 seconds before loading would be completes, as opposed to running them at the same time and finishing loading after only ten seconds. It makes sense from a lifecycle perspective for the component to behave this way; we simply must code accordingly.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM