react加载 HOC:列表中提供的所有props都设置为可选,如果其中任何一个未定义,则显示微调器而不是组件

IT技术 reactjs typescript
2021-05-05 05:30:12

我想创建一个 HOC,我可以在其中提供一组 props 并在其中任何一个未定义时进行检查:如果是这样,则会显示带有微调器的 Loading 组件而不是包装的组件。

这是容易的部分。

由于这些props不能未定义(否则会显示微调器),我希望它们是可选props:

const MyComponentWithLoading = withLoading(MyComponent);

return (
  <MyComponentWithLoading
    loadingFields={['user']}
    user={user} // MyComponent requires "user" to not be undefined, but since it's in "loadingFields" it can actually be undefined
    device={device} // MyComponent requires "device" to not be undefined and it must be so because it's not in "loadingFields"
  />
);

我尝试使用以下代码,但它不起作用。知道如何实现这一目标吗?

import React, {FC, ComponentType, PropsWithChildren} from 'react';
import Loading from '@components/Loading';

type WithLoadingProps<T> = {
  loadingFields?: Array<
    keyof Omit<PropsWithChildren<T & WithLoadingProps<T>>, 'loadingFields'>
  >;
};

const withLoading =
  <T,>(
    Component: ComponentType<T>,
  ): FC<
    Partial<Pick<T, keyof WithLoadingProps<T>['loadingFields']>> &
      Omit<T, keyof WithLoadingProps<T>['loadingFields']> &
      WithLoadingProps<T>
  > =>
  ({loadingFields: loadingData = [], ...otherProps}) => {
    const isLoading = (): boolean => {
      for (const key of loadingData) {
        if (otherProps[key] === undefined) {
          return true;
        }
      }
      return false;
    };
    return isLoading() ? <Loading /> : <Component {...(otherProps as T)} />;
  };

export default withLoading;
1个回答

我认为loadingFields组件上属性最好作为高阶函数的参数放置withLoading

让我们从定义一些类型开始:

type AllowUndefined<T> = {
  [P in keyof T]: T[P] | undefined;
};

type SomeUndefined<T, A extends keyof T> = Omit<T, A> &
  AllowUndefined<Pick<T, A>>;

SomeUndefined类型允许我们类型中的某些字段undefined作为值接受

所以,对于类型

type A = {
    a:number;
    b:string;
}

type B = SomeUndefined<A, 'a'> 将产生一个类型

{
    a:number|undefined
    b:string
}

这将在以后有用。

现在让我们定义withLoading上面讨论的附加参数:

const withLoading = <T, A extends keyof T>(
  Component: ComponentType<T>,
  // readonly is important here so type-inference works correctly
  // require at least one value otherwise t-i breaks down
  loadingFields: readonly [A, ...A[]], 
): FC<SomeUndefined<T, A>> => (props) => {
  const isLoading: boolean = Object.entries(props).some(
    ([k, v]) => loadingFields.includes(k as A) && typeof v === "undefined",
  );
  return isLoading ? (
    <Loading />
  ) : (
    <Component {...(props as PropsWithChildren<T>)} />
  );
};

现在,假设我们有一个简单的组件:

const Foo: FC<{ a: number; b: string }> = ({ a, b }) => (
  <div>
    {[...Array(a)].map((_, i) => (
      <div key={i}>{b}</div>
    ))}
  </div>
);

我们可以包装它并使一些字段可选:

const LoadingFoo = withLoading(Foo, ["a"]);

这意味着我们现在可以使用这个新组件:

<LoadingFoo a={undefined} b="hello" />

我们会看到一条加载消息,

否则:

<LoadingFoo a={10} b="hello" />

我们将看到一个已部署的组件。耶!

代码沙盒示例

编辑

或者,如果您真的想要loadingFields“扩展”组件上props,您可以:

function withLoadingOnComponent<T>(Component: ComponentType<T>) {
  return <A extends keyof T>({
    loadingFields,
    ...props
  }: PropsWithChildren<
    SomeUndefined<T, A> & { loadingFields: readonly [A, ...A[]] }
  >): JSX.Element => {
    const isLoading: boolean = Object.entries(props).some(
      ([k, v]) => loadingFields.includes(k as A) && typeof v === "undefined",
    );
    return isLoading ? <Loading /> : <Component {...(props as any)} />;
  };
}

然后

const FWC = withLoadingOnComponent(Foo);

所以你可以:

<FWC loadingFields={["a"]} a={undefined} b={"hello"} />

我还没有想出如何使它成为可选的。