Skip to content

Instantly share code, notes, and snippets.

@gragland
Last active Jun 25, 2022
Embed
What would you like to do?
React Hook recipe from https://usehooks.com
import React, { useState, useEffect, useCallback } from 'react';
// Usage
function App() {
const { execute, status, value, error } = useAsync(myFunction, false);
return (
<div>
{status === 'idle' && <div>Start your journey by clicking a button</div>}
{status === 'success' && <div>{value}</div>}
{status === 'error' && <div>{error}</div>}
<button onClick={execute} disabled={status === 'pending'}>
{status !== 'pending' ? 'Click me' : 'Loading...'}
</button>
</div>
);
}
// An async function for testing our hook.
// Will be successful 50% of the time.
const myFunction = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const rnd = Math.random() * 10;
rnd <= 5
? resolve('Submitted successfully 🙌')
: reject('Oh no there was an error 😞');
}, 2000);
});
};
// Hook
const useAsync = (asyncFunction, immediate = true) => {
const [status, setStatus] = useState('idle');
const [value, setValue] = useState(null);
const [error, setError] = useState(null);
// The execute function wraps asyncFunction and
// handles setting state for pending, value, and error.
// useCallback ensures the below useEffect is not called
// on every render, but only if asyncFunction changes.
const execute = useCallback(() => {
setStatus('pending');
setValue(null);
setError(null);
return asyncFunction()
.then(response => {
setValue(response);
setStatus('success');
})
.catch(error => {
setError(error);
setStatus('error');
});
}, [asyncFunction]);
// Call execute if we want to fire it right away.
// Otherwise execute can be called later, such as
// in an onClick handler.
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { execute, status, value, error };
};
@spjpgrd
Copy link

spjpgrd commented Jan 14, 2020

This is rad — was looking to do this exact same thing today!

Merged some of what I needed with a lot of what you had:
https://gist.github.com/spjpgrd/eb0902c0566b5a0f91fe67e9bd529975

Heads up, it is TypeScript-ized 🙃

@gragland
Copy link
Author

gragland commented Jan 14, 2020

@spjpgrd
Copy link

spjpgrd commented Jan 14, 2020

@gragland This is a really stripped down version, with overly generic names — but hopefully it gets the point across! (and I didn't mess up when renaming things)

// #region 🔽 Somewhere in the component's local functions

const getData = async (kind?: "retry") => {
    if (kind === "retry") {
        setShowRetrySpinner(true);
    }

    // setIsLoadingData — Each call has their own version of this. Used to get an account of all data fetching calls,
    // and to determine if we should show the initial page load condition, versus a user updating a filter
    setIsLoadingData(true);
    const resp = await getDataService();
    setIsLoadingData(false);

    // Ever increasing wait times to space out requests and also gives the sense of "working harder" for each subsequent request
    if (kind === "retry") {
        setTimeout(() => {
            showRetrySpinner(false);
            setRetryCount(retryCount + 1);
        }, (1500 + (750 * (retryCount * retryCount)))
        );
    }

    return resp;
};

// #endregion

// #region 🔽 Somewhere in the return statement

{retryCount < 3 ?
    <Button
        // Button has it's own internal spinner, but text itself is dealt with here
        showRetrySpinner={showRetrySpinner}
        onClick={async () => {
            const resp = await getData("retry");
            setGroupTopMetrics(resp);
        }}
    >
        {!showRetrySpinner ?
            <>
                {retryCount === 0 &&
                    `Retry`
                }
                {retryCount === 1 &&
                    `Retry Again`
                }
                {retryCount === 2 &&
                    `Retry One More Time`
                }
            </>
            :
            <>
                {retryCount === 0 &&
                    `Trying to get data…`
                }
                {retryCount === 1 &&
                    `Trying again to get data…`
                }
                {retryCount === 2 &&
                    `Trying one more time…`
                }
            </>
        }
    </Button>
    :
    <Button
        onClick={() => {
            openChatSupportWithPredefinedMessage(`👋 I'm having issues seeing data…`);
        }}
    >
        Get Assistance
    </Button>

// #endregion
}

Using the load counts to see if it was an initial load, versus a change, versus an attempt to load again after an error.

We also give the user a way to retry a failed call, after 3 attempts we show a button that connects them with Customer Success.

Error count is just to have, as there are rumblings of ideas to possibly proactively reach out if a page had a sum total of errors that went past a certain threshold. Or some other ideas 🕺

@gragland
Copy link
Author

gragland commented Jan 15, 2020

We also give the user a way to retry a failed call, after 3 attempts we show a button that connects them with Customer Success.

Nice! I think that's a great touch.

@windmaomao
Copy link

windmaomao commented May 15, 2020

Our team used a version of this for a while, and then we removed .finally.

The issue is that execute needs to handle error, therefore, it's better to put setPending(false) inside then. Here's the snippet version we're currently using.


  const execute = useCallback(params => {
    setLoading(true)
    return asyncFunc({ ...funcParams, ...params })
      .then(res => {
        if (!mountedRef.current) return null
        setData(res)
        setError(null)
        setLoading(false)
        return res
      })
      .catch(err => {
        if (!mountedRef.current) return null
        setError(err)
        setLoading(false)
        throw err
      })
  }, [asyncFunc, funcParams])


@xandris
Copy link

xandris commented Jun 24, 2020

You can't rely on useCallback for semantic guarantees about reference stability like that. The React documentation implies such values may be evicted e.g. to reduce memory usage. React could call execute on every render without violating its documented guarantees. That's troubling if you're triggering non-idempotent side effects in execute.

EDIT, two years later: I believe this is no longer the case. I think useCallback used to be based on useMemo. React docs warn to use useMemo as an optimization only and not to rely on its return to be reference stable. But now useCallback has its own implementation and there are no similar warnings around its use. So looks good to me.

@trevithj
Copy link

trevithj commented Sep 30, 2020

Thanks! These recipes are so useful (or is that useFul? )
FYI: We just started using a variation like this:

export const useAsyncWithParams = (asyncFunction) => {
   const [state, dispatch] = useReducer(reducer, {status:'idle', value:null, error:null});
   const isAlive = useRef(true);

   useEffect(() => {
      return () => isAlive.current = false;
   }, []);

   const execute = useCallback((params) => {
      dispatch({ type: 'PENDING' });

      return asyncFunction(params)
         .then(response => {
            if (isAlive.current) {
               dispatch({ type: 'SUCCESS', response });
            }
         })
         .catch(error => {
            if (isAlive.current) {
               dispatch({ type: 'ERROR', error });
            }
         });
   }, [asyncFunction]);

   return { execute, ...state };
};

No immediate callback because the asyncFunction takes params. It ignores responses if the component is unmounted. Also, useReducer is tidier, and only triggers a single update in the component each time. That is: I found that setStatus(...) + setValue(...) each caused a component update.

@sarensw
Copy link

sarensw commented Jan 9, 2021

Thanks! These recipes are so useful (or is that useFul? )
FYI: We just started using a variation like this:

export const useAsyncWithParams = (asyncFunction) => {
   const [state, dispatch] = useReducer(reducer, {status:'idle', value:null, error:null});
   const isAlive = useRef(true);

   useEffect(() => {
      return () => isAlive.current = false;
   }, []);

   const execute = useCallback((params) => {
      dispatch({ type: 'PENDING' });

      return asyncFunction(params)
         .then(response => {
            if (isAlive.current) {
               dispatch({ type: 'SUCCESS', response });
            }
         })
         .catch(error => {
            if (isAlive.current) {
               dispatch({ type: 'ERROR', error });
            }
         });
   }, [asyncFunction]);

   return { execute, ...state };
};

No immediate callback because the asyncFunction takes params. It ignores responses if the component is unmounted. Also, useReducer is tidier, and only triggers a single update in the component each time. That is: I found that setStatus(...) + setValue(...) each caused a component update.

Hi @trevithj, how do you hand over params to useAsyncWithParams?

@sarensw
Copy link

sarensw commented Jan 9, 2021

@trevithj Understood. My bad. Still learning :)

@trevithj
Copy link

trevithj commented Jan 9, 2021

@sarensw Learning is never bad! ;)
FYI: this version uses a single object, so we need to call execute({val1, val2, val3}).
There are smarter ways to pass params so that we don't need the object, but this is nice and simple.

@AlexanderProd
Copy link

AlexanderProd commented Jan 15, 2021

Thanks! These recipes are so useful (or is that useFul? )
FYI: We just started using a variation like this:

export const useAsyncWithParams = (asyncFunction) => {
   const [state, dispatch] = useReducer(reducer, {status:'idle', value:null, error:null});
   const isAlive = useRef(true);

   useEffect(() => {
      return () => isAlive.current = false;
   }, []);

   const execute = useCallback((params) => {
      dispatch({ type: 'PENDING' });

      return asyncFunction(params)
         .then(response => {
            if (isAlive.current) {
               dispatch({ type: 'SUCCESS', response });
            }
         })
         .catch(error => {
            if (isAlive.current) {
               dispatch({ type: 'ERROR', error });
            }
         });
   }, [asyncFunction]);

   return { execute, ...state };
};

No immediate callback because the asyncFunction takes params. It ignores responses if the component is unmounted. Also, useReducer is tidier, and only triggers a single update in the component each time. That is: I found that setStatus(...) + setValue(...) each caused a component update.

Sry if this is maybe a dumb question because im new to using reducers, but what do I need to pass as reducer to useReducer ?

EDIT:
Would this be correct?

function reducer(state, action) {
  switch (action.type) {
    case 'PENDING':
      return { ...state, status: 'pending' };
    case 'SUCCESS':
      return { ...state, status: 'sucess', value: action.value };
    case 'ERROR':
      return { ...state, status: 'error', error: action.error };
    default:
      throw new Error();
  }
}

EDIT 2:
Would it be possible to pass two individual parameters to the async function instead of an object?

@trevithj
Copy link

trevithj commented Jan 17, 2021

@AlexanderProd Your reducer looks fine. We use something very similar.
Parameters could be handled using the argument object. Like in this example. I use an explicit object in the above for simplicity.
@gragland sorry if this thread is off-topic. A useful hook, methinks. 😃

@lgf196
Copy link

lgf196 commented Feb 5, 2021

Your hooks will cause an endless loop

@TranquilMarmot
Copy link

TranquilMarmot commented Jul 20, 2021

I use react-query for async calls like this. It has built in caching, retry logic, etc. One of my favorite libraries!

https://github.com/tannerlinsley/react-query

@Yey007
Copy link

Yey007 commented Sep 21, 2021

I believe there is a bug (or at least very confusing behavior) with this.

If you use useAsync with an async function that returns another function, let's say function F, F will get called out of nowhere. The reason is that here

return asyncFunction()
      .then(response => {
        setValue(response);
        setStatus("success");
      })

setValue will interpret the response (F) as a function that it should run to get the new value. Therefore, F will get called without the user calling it, and the value returned from the hook will be the result of F.

I have reproduced this in a code sandbox.

Here is my proposed change:

const useAsync = (asyncFunction, immediate = true) => {
  const [status, setStatus] = useState('idle');
  const [value, setValue] = useState({value: null});
  const [error, setError] = useState(null);

  // The execute function wraps asyncFunction and
  // handles setting state for pending, value, and error.
  // useCallback ensures the below useEffect is not called
  // on every render, but only if asyncFunction changes.
  const execute = useCallback(() => {
    setStatus('pending');
    setValue({value: null});
    setError(null);

    return asyncFunction()
      .then(response => {
        // If response happens to be a function, passing it in directly can cause problems
        setValue({value: response});
        setStatus('success');
      })
      .catch(error => {
        setError(error);
        setStatus('error');
      });
  }, [asyncFunction]);

  // Call execute if we want to fire it right away.
  // Otherwise execute can be called later, such as
  // in an onClick handler.
  useEffect(() => {
    if (immediate) {
      execute();
    }
  }, [execute, immediate]);

  return { execute, status, value.value, error };
};

Edit: I guess the same issue exists with the error value but I've never seen anyone throw a function before so it's probably not too critical. Still worth fixing if there is time.

@windmaomao
Copy link

windmaomao commented Sep 22, 2021

@Yey007, how can a response be a function?

@trevithj
Copy link

trevithj commented Sep 22, 2021

@windmaomao the relevant snippet from the sandbox:

// An async function for testing our hook. Will be successful 50% of the time.
const myFunction = () => {
  return new Promise((resolve, reject) => {
    resolve(() => console.log("Oh no! This is a bug!"))
  });
};

@Yey007, to clarify - you're saying this will clash with functional updates.

const [state, setState] = useState({});
setState(prevState => {
  return {...prevState, ...updatedValues};
});

This seems right. Nice!

@Yey007
Copy link

Yey007 commented Sep 23, 2021

@trevithj Exactly. This was a nightmare to debug, and I want to save others the pain.

@juandl
Copy link

juandl commented Sep 29, 2021

@gragland I think this hook will cause re-rendering in the parent component as is calling the state more than 3 times. when clean the loading, errors, and when set the items..

here is one I made similar, this hook will setState only 1 time

/**
 * Fetch Query and return result or error
 * @param {Object} params
 * @param {any} params.initialData - Initial items
 * @param {Boolean} params.fetchOnMount - Fetch on mount
 * @param {Promise} params.service - Promise to fetch
 * @param {any} params.query - Query params for service
 * @returns
 */
const useQueryFetch = params => {
  /**
   * params
   */
  const { initialState = null, fetchOnMount, service, query } = params || {};

  /**
   * States
   */
  const [instance, setInstance] = useState({
    loading: true,
    error: null,
    firstMount: false,
    items: initialState
  });

  /**
   * Fetch data from API
   */
  const onFetch = async () => {
    let _instance = {
      ...instance
    };

    try {
      //Fetch service
      const { data } = await service(query);

      //Set items
      _instance.items = data;
    } catch (err) {
      //Set error response data axios type
      if (err?.response?.data) {
        _instance.error = err.response.data;
      } else {
        //Add internal error
        _instance.error = { internal: 'Something fail, try again' };
      }
    }

    //Close loading
    if (!_instance.error) _instance.loading = false;

    //Add first mount
    if (!_instance.firstMount) _instance.firstMount = true;

    //Complete query instance
    setInstance(_instance);
  };

  useEffect(() => {
    /**
     * Fetch data on mount
     */
    if (fetchOnMount) onFetch();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return [onFetch, useMemo(() => instance, [instance])];
};

@juhagenah
Copy link

juhagenah commented Nov 13, 2021

To me it seems that an unwanted line break was introduced in the description on useHooks.com in the typescript variant leading to the file not compiling:
"idle" | "pending" | "success" | "error"

("idle");
should be in one line, I think.
Great stuff nevertheless!!!

@devgioele
Copy link

devgioele commented Mar 16, 2022

@xandris pointed out a very important issue here! For example, I pass a http fetch as the execute function, which then triggers other effects. In such a case, the execute function is called 3 times instead of just 1.

@xandris
Copy link

xandris commented Mar 16, 2022

@devgioele i barely remember this! i think the issue is the function returned from useCallback isn't guaranteed to be === for the same dependencies...maybe a plain ref is right right answer; something guaranteed to be stable. although i've never seen any evidence of this 'cache eviction behavior', i just remember the React team left that option open for them. your issue might also be caused by rerenders higher up the tree though? when things get real deep it's easy to miss like key attributes, or subtrees might be getting unmounted and remounted by effects... it could also be parameters to the useAsync hook itself, maybe those aren't reference stable?

@devgioele
Copy link

devgioele commented Mar 17, 2022

Thanks for the quick reply @xandris! The actual problem was that not passing a reference stable function to useAsync caused infinite calls. To avoid that this function is recreated on each render, my solution is to wrap this function first with useCallback and then passing it to useAsync.

The fact that execute was called 3 times... that was because I was actually using it in 3 different places, but didn't realize it!

@TusharShahi
Copy link

TusharShahi commented Jun 25, 2022

Is it ideal to use a function as a useCallback dependency? Function is created fresh in every render (unless handled).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment