Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
July 26, 2021 11:20 pm GMT

How to cook up a powerful React async component using hooks

Photo by Adrian Infernus on Unsplash

Introduction

It's amazing what you can cook up by mixing together a few hooks. I thought I'd put together an overview of how to make a powerful useAsync hook that allows you to do all sorts of cool progress animations.

Here's a sneak peek at it updating multiple areas of a React app:

As you can see multiple parts of the interface update independently and we can restart the operation by changing a few deps - which cancels the previous operation.

The Code

For the purpose of this hook we are going to combine the useMemo, useState, and useRef hooks to produce a useAsync hook that takes an async function which gets passed some utility functions which can be used to provide intermediate results as it executes, check whether the function should cancel and restart the operation.

Firstly what we are after is producing a component that is made up of multiple parts that are independently updated. For testing, we will write an async function that runs two jobs in parallel and then combines the results at the end.

A basic wrapper App might look like this:

export default function App() {    const {        progress1 = null,        progress2 = null,        done = null    } = useAsync(runProcesses, [])    return (        <div className="App">            <div>{progress1}</div>            <div>{progress2}</div>            <div>{done}</div>        </div>    )}

The one in the CodeSandbox is a bit fancier, using Material UI components, but it's basically this with bells on.

runProcesses is the actual async function we want to run as a test. We'll come to that in a moment. First let's look at useAsync.

useAsync

So here's the idea:

  • We want to return an object with keys that represent the various parts of the interface
  • We want to start the async function when the dependencies change (and run it the first time)
  • We want the async function to be able to check whether it should cancel after it has performed an async operation
  • We want the async function to be able to supply part of the interface and have it returned to the outer component for rendering
  • We want to be able to restart the process by calling a function

Lets map those to standard hooks:

  • The return value can be a useState({}), this will let us update the result by supplying an object to be merged with the current state
  • We can use useMemo to start our function immediately as the dependencies change
  • We can check whether we should cancel by using a useRef() to hold the current dependencies and check if it's the same as the dependencies we had when we started the function. A closure will keep a copy of the dependencies on startup so we can compare them.
  • We can use another useState() to provide an additional "refresh" dependency
function useAsync(fn, deps = [], defaultValue = {}) {    // Maintain an internal id to allow for restart    const [localDeps, setDeps] = useState(0)    // Hold the value that will be returned to the caller    const [result, setResult] = useState(defaultValue)    // If the result is an object, decorate it with    // the restart function    if(typeof result === 'object') {        result.restart = restart    }    // Holds the currently running dependencies so    // we can compare them with set used to start    // the async function    const currentDeps = useRef()    // Use memo will call immediately that the deps    // change    useMemo(() => {        // Create a closure variable of the currentDeps        // and update the ref        const runningDeps = (currentDeps.current = [localDeps, myId, ...deps])        // Start the async function, passing it the helper        // functions        Promise.resolve(fn(update, cancelled, restart)).then((result) => {            // If the promise returns a value, use it            // to update what is rendered            result !== undefined && update(result)        })        // Closure cancel function compares the currentDeps        // ref with the closed over value        function cancelled() {            return runningDeps !== currentDeps.current        }    }, [localDeps, myId, ...deps]) // The dependencies    return [result, restart]    // Update the returned value, we can pass anything    // and the useAsync will return that - but if we pass    // an object, then we will merge it with the current values    function update(newValue) {        setResult((existing) => {            if (                typeof existing === "object" &&                !Array.isArray(existing) &&                typeof newValue === "object" &&                !Array.isArray(newValue) &&                newValue            ) {                return { ...existing, ...newValue }            } else {                return newValue            }        })    }    // Update the local deps to cause a restart    function restart() {        setDeps((a) => a + 1)    }}

The Test Code

Ok so now we need to write something to test this. Normally your asyncs will be server calls and here we will just use a delayed loop to simulate this. Like a series of server calls though we will have a value being calculated and passed to 2 asynchronous functions that can run in parallel, when they have both finished we will combine the results. As the functions run we will update progress bars.

async function runProcesses(    update: UpdateFunction,    cancelled: CancelledFunction,    restart: RestartFunction) {    update({ done: <Typography>Starting</Typography> })    await delay(200)    // Check if we should cancel    if (cancelled()) return    // Render something in the "done" slot    update({ done: <Typography>Running</Typography> })    const value = Math.random()    const results = await parallel(        progress1(value, update, cancelled),        progress2(value, update, cancelled)    )    // Check if we should cancel    if (cancelled()) return    return {        done: (            <Box>                <Typography variant="h6" gutterBottom>                    Final Answer: {(results[0] / results[1]).toFixed(1)}                </Typography>                <Button variant="contained" color="primary" onClick={restart}>                    Restart                </Button>            </Box>        )    }}

This function does pretty much what I mentioned, it calculates a value (well it's a random!) - passes it to two more functions and when they are done it returns something to render in the done slot.

As you can see we take an update function that we can use to update the elements of the component. We also have a cancelled function that we should check and return if it is true.

Here is the code for one of the progress functions. It multiplies a value with a delay to make it async. Every step it updates a progress bar and finally replaces it with the result.

async function progress1(    value: number,    update: UpdateFunction,    cancelled: CancelledFunction) {    for (let i = 0; i < 100; i++) {        value *= 1.6 - Math.random() / 5        await delay(50)        // Check if we should cancel        if (cancelled()) return        // Render a progress bar        update({            progress1: (                <LinearProgress                    variant="determinate"                    color="primary"                    value={i}                />            )        })    }    value = Math.round(value)    // When done, just render the final value    update({ progress1: <Typography>{value}</Typography> })    return value}

Utilities

We use a delay and a parallel function here, this is what they look like:

// Promise for a delay in millisecondsfunction delay(time = 100) {    return new Promise((resolve) => setTimeout(resolve, time))}// Promise for the results of the parameters which can be// either functions or promisesfunction parallel(...items) {    return Promise.all(        items.map((item) => (typeof item === "function" ? item() : item))    )}

Conclusion

Well that about wraps it up. We've taken 3 standard hooks and created a powerful hook to enable complex asynchronous components.

The code (in TS and JS) is in the linked CodeSandbox at the top of this article.


Original Link: https://dev.to/miketalbot/how-to-make-a-powerful-react-async-component-122d

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To