【react.js + hooks】使用 useLoading 控制加载

在页面上 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:plusminus,即递增和递减。
此外,我还为 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>
    </>
  )
}

你可能感兴趣的:(react.js,网络,前端)