Skip to content

Instantly share code, notes, and snippets.

@gragland gragland/use-async.jsx
Last active Jan 15, 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, pending, value, error } = useAsync(myFunction, false);
return (
<div>
{value && <div>{value}</div>}
{error && <div>{error}</div>}
<button onClick={execute} disabled={pending}>
{!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 [pending, setPending] = useState(false);
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(() => {
setPending(true);
setValue(null);
setError(null);
return asyncFunction()
.then(response => setValue(response))
.catch(error => setError(error))
.finally(() => setPending(false));
}, [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, pending, value, error };
};
@spjpgrd

This comment has been minimized.

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

This comment has been minimized.

Copy link
Owner Author

gragland commented Jan 14, 2020

@spjpgrd

This comment has been minimized.

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

This comment has been minimized.

Copy link
Owner 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.

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.