Skip to content

Instantly share code, notes, and snippets.

@gragland

gragland/use-async.jsx

Last active Aug 3, 2020
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

This comment has been minimized.

Copy link

@spjpgrd 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

This comment has been minimized.

Copy link
Owner Author

@gragland gragland commented Jan 14, 2020

@spjpgrd

This comment has been minimized.

Copy link

@spjpgrd 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

This comment has been minimized.

Copy link
Owner Author

@gragland 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

This comment has been minimized.

Copy link

@windmaomao 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

This comment has been minimized.

Copy link

@xandris 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.