在页面上 loading(加载)的效果十分常见,在某些场景下,一个页面上甚至可能有特别多的 loading 存在,此时为每一个 loading 专门创建一个 state 显然太过繁琐,不如试试写一个 useLoading 来集中管理!
当页面上有众多的 loading 时,我们需要能区分它们,因此 useLoading 存储的 state 应当是 KV 式 或者数组式,KV 更方便,我们将采用 KV 式,K 代表 loading 的标识名称,V代表是否 loading。
useLoading 应当提供哪些 api 呢?返回 state 和 setState 是必要的,此外,提供根据 K 的 setLoading 以及 根据 K 的 onloading 和 unloading 也是 非常使用的。
既然已经确定了 state 为 KV 式,初始化给个对象即可。
声明 UseLoading 的入参为初始 state ,并定义其为泛型,默认为 Record
,并按照 useHooks 的习惯,返回一个 只读 的数组,第一项为 state,第二项为 set,第三项为 onloading,第四项为 unloading。
此外,我们需要为 set 额外定义一个接口,用于 (K,V)
和 ({k,v})
两种入参形式的函数重载。
export interface SetLoading<
T = Record<string, Boolean | number>,
K extends keyof T = keyof T
> {
(key: K, value?: boolean): void;
(key: K, setAction: (pre: boolean | number)=> boolean): void;
(state: Record<K, boolean>): void;
(setAction: (pre: T) => T): void;
}
export interface UseLoading<T = Record<string, boolean>> {
(loadingMap: T): readonly [
T,
SetLoading,
(key: keyof T) => void,
(key: keyof T) => void
];
}
export const useLoading: UseLoading = <T = Record<string, boolean | number>>(
initialLoadingMap: T
) => {
const [loading, _setLoading] = useState<T>(initialLoadingMap);
const setLoading: SetLoading<T, keyof T> = (args1 , value = true) => {
if (typeof args1 === "object") {
_setLoading((pre) => ({ ...pre, ...args1 }));
return;
} else if (typeof args1 === "function") {
_setLoading((pre) => args1(pre));
return;
} else {
const key = args1;
if (typeof value === "function") {
_setLoading((pre) => ({ ...pre, [key]: value(pre[key]) }));
} else {
_setLoading((pre) => ({ ...pre, [key]: value }));
}
}
};
const onLoading = (key: keyof typeof loading) => {
_setLoading((pre) => ({ ...pre, [key]: true }));
};
const unLoading = (key: keyof typeof loading) => {
_setLoading((pre) => ({ ...pre, [key]: false }));
};
return [
loading,
setLoading,
onLoading,
unLoading,
] as unknown as ReturnType<UseLoading>;
};
到这里,基础的 useLoading 就实现了,以下是简单的使用示例:
export default function Demo(){
const [loading, setLoading] = useLoading({
button1: false,
button2: false,
})
const handleClick = (key: 'button1'|'button2') => {
setLoading()
}
return(
<>
<button onClick={() => setLoading('button1')}>
{loading.button1? 'loading':'button1'}
</button>
<button onClick={() => setLoading('button1')}>
{loading.button2? 'loading':'button2'}
</button>
</>
)
}
有时候,我们的 loading 只是效果,并不阻止用户操作,那么当用户连续进行点击等操作时,我们希望 loading 效果应当延续下去,此时只使用上面的 useLoding 显然乏力,我们需要额外维护一个具有计数性质的 state(数字数组,Promise数组等) 配合使用,这将使代码变得异常臃肿。
换位思考一下,我们需要计数的特性,useLoding 使用的 state 是 Boolean,那么有没有办法可以同时兼备布尔值和数字的特性呢?在 JS/TS 中,boolean 本身就支持加减操作,加减后会隐式转换为数字,答案已经呼之欲出了!允许我们的 useLodng 使用数字作为每个 loading 的值,即可完美的升级 useLoading,并完全兼容基础版的 useLoading,我们只需要做一些小小的改变:
在这里,将 boolean
都替换为 boolean | number
即可。
其次,我们提供了2个额外的 api:plus
和 minus
,即递增和递减。
此外,我还为 useLoading 新加了一个 returnType 入参的重载,因为有些人可能更偏爱对象而不是数组,比如笔者自己。
export interface UseLoading<T = Record<string, boolean | number>> {
(loadingMap: T): readonly [
T,
SetLoading,
(key: keyof T) => void,
(key: keyof T) => void,
(key: keyof T) => void,
(key: keyof T) => void
];
(loadingMap: T, returnType: "object"): Readonly<{
values: T;
set: SetLoading;
on: (key: keyof T) => void;
un: (key: keyof T) => void;
plus: (key: keyof T) => void;
minus: (key: keyof T) => void;
}>;
}
export interface SetLoading<
T = Record<string, boolean | number>,
K extends keyof T = keyof T
> {
(key: K, value?: boolean | number): void;
(key: K, setAction: (pre: boolean | number) => boolean | number): void;
(state: Record<K, boolean | number>): void;
(setAction: (pre: T) => T): void;
}
在这里,实现上与 基础的 useLoading 几乎完全相同,只是多了几个新的返回和一套对象形式的返回。
// @ts-ignore
export const useLoading: UseLoading = <T = Record<string, boolean | number>>(
loadingMap: T,
returnType: "array" | "object" = "array"
) => {
const [loading, _setLoading] = useState(loadingMap);
const setLoading: SetLoading<T, keyof T> = (args1, value = true) => {
if (typeof args1 === "object") {
_setLoading((pre) => ({ ...pre, ...args1 }));
return;
} else if (typeof args1 === "function") {
_setLoading((pre) => args1(pre));
return;
} else {
const key = args1;
if (typeof value === "function") {
_setLoading((pre) => ({ ...pre, [key]: value(pre[key]) }));
} else {
_setLoading((pre) => ({ ...pre, [key]: value }));
}
}
};
const onLoading = (key: keyof typeof loading) => {
_setLoading((pre) => ({ ...pre, [key]: 1 }));
};
const unLoading = (key: keyof typeof loading) => {
_setLoading((pre) => ({ ...pre, [key]: 0 }));
};
const plusLoading = (key: keyof typeof loading) => {
_setLoading((pre) => ({ ...pre, [key]: (pre[key] as number) + 1 }));
};
const minusLoading = (key: keyof typeof loading) => {
_setLoading((pre) => ({ ...pre, [key]: (pre[key] as number) - 1 }));
};
if (returnType === "array") {
return [
loading,
setLoading,
onLoading,
unLoading,
plusLoading,
minusLoading,
] as const;
} else {
return {
values: loading,
set: setLoading,
on: onLoading,
un: unLoading,
plus: plusLoading,
minus: minusLoading,
} as const;
}
};
现在,一个进阶版的 useLoading 就完成了,你完全可以把它当作普通的 useLoading 穿 boolean 用,如果有计数的需要你就可以把它当数字用:
function sleep<T>(time: number) {
return new Promise<void>(function (resolve) {
setTimeout(() => {
resolve();
}, time);
});
}
export default function Demo(){
const { values: loading, plus, minus } = useLoading(
{ button1: false }, 'object');
const click = async () => {
plus('button1')
await sleep(1000);
minus('button1')
}
}
return(
<>
<button onClick={click}>
{loading.button1? 'loading':'button1'}
</button>
</>
)
}