kwhitejr

React Hook - useAsyncFunc

March 09, 2021

Source code and demo for generic async operation React hook. Live example.

Want to suggest an improvement? I’m all ears! Please file an issue or open a PR!

Usage

import useAsyncFunc from "useAsyncFunc";

const myAsyncFunc = (arg1, arg2) => {
  // do work
};

const App = () => {
  const [{ isLoading, isError, data }, setArgs] = useAsyncFn(myAsyncFunc, ['firstArg', 'secondArg']);

  return (
    {data && <div>{data}</div>}
    {isLoading && <LoadingIcon/>}
    {isError && <ErrorMsg/>}
  );
};

Overview

This useAsyncFunc React hook stands on the shoulders of giants. Robin Wieruch’s excellent How to fetch data with React Hooks walks you step-by-step through the creation of a robust useDataApi hook, which provides an exhaustively capable data fetching hook.

The purpose of this post is to further build on that work by genericizing it to run any async function (and not just fetch(myUrl)).

useAsyncFunc

/**
 * @param {function} fn   - The asynchronous function to be called
 * @param {Array} fnArgs  - Optionally, the arguments to be passed to `fn`, expressed as an array
 * @param {*} initialData - Optionally, default value(s) to returned as `data`
 * @returns {[state, setArgs]}
 */
const useAsyncFunc = (fn, fnArgs = [], initialData) => {

  // useState manages the async func arguments.
  const [args, setArgs] = useState(fnArgs);

  // useReducer manages the local complex state of the async func hook's lifecycle.
  // See the source code for the full reducer!
  // NOTE: it'd be easy to modify the reducer to fit your needs.
  const [state, dispatch] = useReducer(asyncReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  useEffect(() => {
    let didCancel = false;

    const callAsyncFn = async () => {
      // Begin each async func call with a clean slate.
      dispatch({ type: 'ASYNC_INIT' });

      try {
        // Because this is a generic hook for any async func,
        // simply call the function with the args!
        const result = await fn(...args);

        // Set success with result
        if (!didCancel) {
          dispatch({ type: 'ASYNC_SUCCESS', payload: result });
        }
      } catch (error) {
        // Otherwise, set failure
        if (!didCancel) {
          dispatch({ type: 'ASYNC_FAILURE' });
        }
      }
    };

    // Actual invocation
    callAsyncFn();

    // Cleanup function to optionally prevent setting state for unmounted component.
    // Ref: https://reactjs.org/docs/hooks-effect.html#example-using-hooks-1
    // Ref: https://www.robinwieruch.de/react-warning-cant-call-setstate-on-an-unmounted-component
    return () => {
      didCancel = true;
    };

    // useEffect will consider `fn` and `args` when deciding whether to skip a re-render.
    // In short, skip re-render if `fn` or `args` do not change.
  }, [fn, args]);

  // `state` provides the status updates: { isLoading, isError, data }
  // `setArgs` allows you to update the arguments passed to the async func
  return [state, setArgs];
};

Conclusion

Really, that is it! Again, this is a slight modification to genericize Robin Wieruch’s already-thorough useDataApi() hook. My other small contribution is a basic test suite. Enjoy!


Kevin lives in Oregon brewing his coffee, walking his puppy, and slowly learning to program better.

© 2021, Built with Gatsby