如何优雅地处理 loading
让我们想象一个场景:
“下班顺路买一斤包子带回来,如果看到卖西瓜的,买一个。”
现在,让我们用 react
来实现一下:
function GoHome() {
const [loading, setLoading] = useState(false)
const goForward = async () => {
setLoading(true)
const stuff = await lookAround()
switch (stuff) {
case '包子':
await buy(2, '斤', '包子')
break
case '西瓜':
await buy(1, '个', '包子')
break
default:
break
}
setLoading(false)
}
// do something with loading
}
这样显然是不行的,因为异步方法可能会抛错,导致 setLoading(false)
被跳过,那么我们可能会:
const goForward = async () => {
setLoading(true)
try {
// ...
} finally {
setLoading(false)
}
}
这样实在是不够优雅。
what about this:
function GoHome() {
const [loading, withLoading] = useLoading()
const goForward = withLoading(async () => {
const stuff = await lookAround()
switch (stuff) {
case '包子':
await buy(2, '斤', '包子')
break
case '西瓜':
await buy(1, '个', '包子')
break
default:
break
}
})
// do something with loading
}
以此延伸,我们通常会有这样的需求:点击按钮时发送请求,如果请求超过 500 ms, 再展示 loading
function HelloWorld() {
return <Button onClick={withLoading(api, 500)}>
点我一刀 999
</Button>
}
实现不复杂,直接看全部代码
import { clamp } from 'lodash-es'
import { useCallback, useState } from 'react'
const InfiniteTimeout = 2 ** 31 - 1
/**
* 同一个 withLoading 可以包装多个函数,
* 同一个 withLoading 包装的所有函数全部执行完毕后才会关闭 loading;
*
* @usage
* ```tsx
* function Comp() {
* const [ loading, withLoading ] = useLoading()
*
* const { data } = useSWR('xxx', withLoading(asyncTask1))
*
* return <>
* <Spin spinning={loading} />
*
* <Button
* onClick={withLoading(asyncTask2)}
* >
* async task
* </Button>
* </>
* }
* ```
*/
export function useLoading() {
const [flag, setFlag] = useState(0)
/**
* @param fn 需要包装的函数
* @param delayMs 延迟显示 loading (!!! 而非延迟执行函数)
*/
const withLoading = useCallback(
<Arg extends unknown[], Res>(
fn: (...args: Arg) => Res | Promise<Res>,
delayMs = 500
) =>
async (...args: Arg) => {
let timer = -1
if (delayMs > 0) {
timer = +setTimeout(() => {
timer = -1
setFlag((prev) => prev + 1)
}, clamp(delayMs, 0, InfiniteTimeout))
} else {
setFlag((prev) => prev + 1)
}
try {
return await fn(...args)
} finally {
clearTimeout(timer)
if (timer < 0) {
setFlag((prev) => prev - 1)
}
}
},
[]
)
return [
loading: flag > 0,
withLoading,
] as const
}
后记:代码的实现很简单,但我感觉从代码风格上来说,由原来的命令式,改为了声明式,更 "react" 了,所以感觉很有意义,就此记录。(当然不会说是为了水一篇文了)