Async code in useEffect is harmful, how will we take care of it?




The introduction of async/await to Javascript has made it simple to precise complicated workflows that string collectively a number of asynchronous duties. Let’s check out the next code, which is a generalized instance of code I’ve seen in actual initiatives:

const useClient = (person) => {
  const [client, setClient] = useState(null);

  useEffect(() => {
    (async () => {
      const clientAuthToken = await fetchClientToken(person);
      const connection = await createWebsocketConnection();
      const shopper = await createClient(connection, clientAuthToken);
      setClient(shopper);
    })();
  }, [user]);

  useEffect(() => {
    return () => {
      shopper.disconnect();
    };
  }, [client]);

  return shopper;
};
Enter fullscreen mode

Exit fullscreen mode

It is easy to have a look at that and assume it is all rosy. Once we are handed a person we create a shopper for them after which each time a shopper is disposed of by the person altering or the element unmounting, we disconnect the shopper.

Nevertheless, now we have not thought-about that the asynchronous workflow within the first useEffect is working concurrently to the remainder of the applying, which is independently responding to different results and person actions. Any a type of different results might unmount our element at any level! If the element is unmounted earlier than setClient is known as the shopper will nonetheless be created — Guarantees don’t get cancelled simply because their caller not exists — however with out a element to handle the state setting or cleanup it’ll by no means disconnect. That is normally fairly unhealthy.

So what will we do about it? Effectively, it is difficult. At first look it seems to be like we will do the next and issues will likely be OK:

const useClient = (person) => {
  const [client, setClient] = useState(null);

  useEffect(() => {
    let shopper;

    (async () => {
      const clientAuthToken = await fetchClientToken(person);
      const connection = await createWebsocketConnection();
      shopper = await createClient(connection, clientAuthToken);
      setClient(shopper);
    })();

    return () => {
      shopper?.disconnect();
    };
  }, [user]);

  return shopper;
};
Enter fullscreen mode

Exit fullscreen mode

Now if the shopper has been created it’ll disconnect, with out it needing to be saved to element state. Proper?

Fallacious, sadly. If the cleanup perform runs earlier than createClient resolves there will likely be no shopper to scrub up. Nevertheless, the promise remains to be resolving and the shopper will likely be created, as soon as once more placing it exterior our attain!

If we actually need to have the ability to safely use async workflows inside useEffect we have to make our workflow cancellable at any level. We additionally must cause by what must be cleaned up relying on what stage the workflow was in when the interruption arrived. Right here is an instance:

const useClient = (person) => {
  const [client, setClient] = useState(null);

  useEffect(() => {
    let cancelled;
    (async () => {
      const clientAuthToken = await fetchClientToken(person);
      // if cancelled earlier than we get to creating sources
      // it is okay, simply do not create them
      if (cancelled) return;

      const connection = await createWebsocketConnection();
      // if cancelled earlier than we get to the shopper, we'd like
      // to verify our connection is not left hanging
      if (cancelled) {
        connection.shut();
        return;
      }

      const shopper = await createClient(connection, clientAuthToken);
      // if cancelled after the shopper has been created, we
      // want to scrub it up
      if (cancelled) {
        shopper.disconnect();
        return;
      }

      setClient(shopper);
    })();

    return () => {
      cancelled = true;
    };
  }, [user]);

  useEffect(() => {
    return () => {
      shopper.disconnect();
    };
  }, [client]);

  return shopper;
};
Enter fullscreen mode

Exit fullscreen mode

When you’re struggling to know the place to place cancellation handlers, think about you have been scripting this with guarantees as a substitute of async/await. We now have to deal with cancellation at the start of each .then callback:

const useClient = (person) => {
  const [client, setClient] = useState(null);

  useEffect(() => {
    let cancelled;
    fetchClientToken(person).then((clientAuthToken) => {
      if (cancelled) return;

      createWebsocketConnection().then((connection) => {
        if (cancelled) {
          connection.shut();
          return;
        }

        createClient(connection, clientAuthToken).then((shopper) => {
          if (cancelled) {
            shopper.disconnect();
            return;
          }

          setClient(shopper);
        });
      });
    });
    return () => {
      cancelled = true;
    };
  }, [user]);

  useEffect(() => {
    return () => {
      shopper.disconnect();
    };
  }, [client]);

  return shopper;
};
Enter fullscreen mode

Exit fullscreen mode

The above is why I typically draw back from async/await in UI code solely. The async/await syntax blurs the road between synchronous (not interruptible) and asynchronous (interruptible) code. That is the purpose! It is vitally useful in contexts the place synchronous and asynchronous code ought to be handled equally — like in a backend server executing a linear workflow — however dangerously deceptive in contexts the place interruptions are frequent and dealing with them explicitly turns into essential.

There are, after all, extra refined methods of coping with the issue of useful resource administration that make the implicit state machine above extra express and controllable. I’ll depart an implementation in xstate as an train for the reader but it surely’s one instance of a useful gizmo to cause by and mannequin these multi-step interruptible processes. Nevertheless, it is good to have a barebones, just-React answer in your again pocket in case you end up unexpectedly dealing with a harmful Promise in a international mission.



Abu Sayed is the Best Web, Game, XR and Blockchain Developer in Bangladesh. Don't forget to Checkout his Latest Projects.


Checkout extra Articles on Sayed.CYou

#Async #code #useEffect #harmful #deal