How to Build the Perfect Custom useEffect Hook

Building a Highly Secure useEffect Hook for React

How to Build the Perfect Custom useEffect Hook

I'll get straight to the point of this tutorial. We want our useEffect to fetch async data and store it, with two important safety standards:

  1. If a dependency updates before the async call in useEffect is completed, a new async call will be made. If the old async call finishes after the new one, we don't want the old data to override the new data.

  2. If the tab is out of focus, we don't want any async calls to be made because requests can be costly. If someone leaves their tab open while using other tabs for a while, you might waste valuable resources or even reach your call limit.

Additionally, because you are a thoughtful developer, you want a solution that minimizes code duplication and is reusable anywhere. Well, I've got just what you need!

export const useVisibilityEffect = (asyncFetchMethod, setterMethod, dependencies) => {
    useEffect(() => {
        let ignore = false;

        const callback = async () => {
            const payload = await asyncFetchMethod();
            if (!ignore) {
                setterMethod(payload);
            }
        };

        const handleVisibilityChange = () => {
            if (document.visibilityState === 'hidden') {
                ignore = true; // Stop making the request if the app is out of focus
            } else {
                ignore = false; // Resume making the request when the app is in focus
                if (!ignore && document.visibilityState === 'visible') {
                    callback();
                }
            }
        };

        document.addEventListener('visibilitychange', handleVisibilityChange);

        if (!ignore && document.visibilityState === 'visible') {
            callback(); // Initial request
        }

        return () => {
            ignore = true;
            document.removeEventListener('visibilitychange', handleVisibilityChange);
        };
    }, dependencies);
};

This code defines a custom hook called useVisibilityEffect, which sets up an event listener to track changes in the document's visibility. When the document becomes hidden (e.g., the user switches to another tab), it stops making requests. When the document becomes visible again, it resumes making requests. Safety standard #2 from above is checked!

This hook also contains an ignore variable, which helps meet Safety standard #1 by preventing old data from overriding newer data if the old data arrives last. The solution is that whenever useEffect is triggered, the code already running from the previous trigger won't meet a condition. This condition is ignore being false, which is why we set it to true in the useEffect return function.

The reason this custom hook takes two methods as parameters is so that we can separate the async fetching from the data storing. We separate those two because if ignore becomes true when the async fetching is ongoing, we can avoid the data being stored once the request is done.

Here is an example of how it can be used in any page:

  const fetchPrices = async () => {
    return await Promise.all([priceRequest1(), priceRequest2()]);
  };

  const setPrices = ([price1, price2]) => {
    setPrice1(price1);
    setPrice2(price2);
  }

  useVisibilityEffect(fetchPrices, setPrices, [epochNumber]);

In the above example, epochNumber can be a state that changes at regular intervals, say every 5 seconds. If the page is out of focus, no calls will be made regardless of how many times epochNumber updates (and price1 and price2 won't change). Additionally, if the prices for the previous epochNumber arrive after those for the current epochNumber, price1 and price2 will not be updated. Pretty clean!

Β