53
loading...
This website collects cookies to deliver better user experience
useMemo
, useState
, and useRef
hooks to produce a useAsync
hook that takes an async function that is 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.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>
)
}
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
.useState({})
, this will let us update the result by supplying an object to be merged with the current stateuseMemo
to start our function immediately as the dependencies changeuseRef()
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.useState()
to provide an additional "refresh" dependency
// Javascript version (both JS/TS in CodeSandbox)
const myId = Date.now() // Helps with Hot Module Reload
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
}
// 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) {
if(cancelled()) return
setResult((existing) => {
if (
typeof existing === "object" &&
!Array.isArray(existing) &&
typeof newValue === "object" &&
!Array.isArray(newValue) &&
newValue
) {
return { ...existing, ...newValue }
} else {
return newValue
}
})
}
}, [localDeps, myId, ...deps]) // The dependencies
return result
// Update the local deps to cause a restart
function restart() {
setDeps((a) => a + 1)
}
}
// TypeScript version (both JS/TS in CodeSandbox)
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>
)
}
}
done
slot.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
.// TypeScript version (both JS/TS in CodeSandbox)
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
}
delay
and a parallel
function here, this is what they look like:// Promise for a delay in milliseconds
function delay(time = 100) {
return new Promise((resolve) => setTimeout(resolve, time))
}
// Promise for the results of the parameters which can be
// either functions or promises
function parallel(...items) {
return Promise.all(
items.map((item) => (typeof item === "function" ? item() : item))
)
}