学习TypeScript + React:组件模式

97 阅读4分钟

这个列表是React与TypeScript合作时的组件模式的集合。把它们看作是TypeScript + React指南的延伸,涉及到整体概念和类型。这个列表在很大程度上受到chantastic的原始React模式列表的启发。

与chantastic的指南相反,我主要使用现代的React,所以功能组件和--如果有必要--钩子。我也只关注类型。

基本的功能组件#

当使用没有任何道具的函数组件时,你没有必要使用额外的类型。一切都可以被推断出来。在老式的函数中(我更喜欢),以及箭头函数中。

function Title() {
  return <h1>Welcome to this application</h1>;
}

道具#

在使用props时,我们通常根据我们正在编写的组件来命名props,用Props-suffix。不需要使用FC 组件封装器或类似的东西。

type GreetingProps = {
  name: string;
};

function Greeting(props: GreetingProps) {
  return <p>Hi {props.name} 👋</p>
}

解构使其更具有可读性

function Greeting({ name }: GreetingProps) {
  return <p>Hi {name} 👋</p>;
}

默认道具#

与其像在基于类的React中那样设置默认的props,不如给props设置默认值来的简单。我们用默认值可选的方式标记道具(见问号操作符)。默认值可以确保name ,永远不会未定义。

type LoginMsgProps = {
  name?: string;
};

function LoginMsg({ name = "Guest" }: LoginMsgProps) {
  return <p>Logged in as {name}</p>;
}

儿童#

与其使用FCFunctionComponent 帮助器,我们更愿意明确地设置children ,因此它遵循与其他组件相同的模式。我们将children 设置为React.ReactNode 类型,因为它接受大多数(JSX元素、字符串等)。

type CardProps = {
  title: string;
  children: React.ReactNode;
};

export function Card({ title, children }: CardProps) {
  return (
    <section className="cards">
      <h2>{title}</h2>
      {children}
    </section>
  );
}

当我们明确地设置children ,我们也可以确保我们永远不会传递任何孩子。

// This throws errors when we pass children
type SaveButtonProps = {
  //... whatever
  children: never
}

请看我的论据,为什么我不在这个编辑器中使用FC

WithChildren帮助类型#

一个自定义的帮助器类型可以帮助我们更容易地设置children

type WithChildren<T = {}> = 
  T & { children?: React.ReactNode };

type CardProps = WithChildren<{
  title: string;
}>;

这与FC 非常相似,但有了{} 的默认通用参数,它可以更加灵活。

// works as well
type CardProps = { title: string } & WithChildren;

如果你使用Preact,你可以使用h.JSX.ElementVNode 作为类型,而不是React.ReactNode

将属性分散到HTML元素中#

将属性扩散到HTML元素是一个很好的功能,你可以确保你能够设置一个元素所具有的所有HTML属性,而不需要预先知道你要设置哪些属性。你可以把它们传递出去。这里有一个按钮包装组件,我们在这里传播属性。为了获得正确的属性,我们通过JSX.IntrinsicElements ,访问一个button's props。这包括children ,我们把它们分散开来。

type ButtonProps = JSX.IntrinsicElements["button"];

function Button({ ...allProps }: ButtonProps) {
  return <button {...allProps} />;
}

预设属性#

假设我们想把type 预设为button ,因为默认行为submit 试图发送一个表单,而我们只想让事情可点击。我们可以通过从按钮道具集合中省略type 来获得类型安全。

type ButtonProps =
  Omit<JSX.IntrinsicElements["button"], "type">;

function Button({ ...allProps }: ButtonProps) {
  return <button type="button" {...allProps} />;
}

// 💥 This breaks, as we omitted type
const z = <Button type="button">Hi</Button>; 

样式化组件#

不要与CSS-in-JS库中的styled-components相混淆。我们想根据我们定义的道具来设置CSS类。例如,一个新的type 属性,允许被设置为primarysecondary

我们省略原来的typeclassName ,并与我们自己的类型相交。

type StyledButton = Omit<
  JSX.IntrinsicElements["button"],
  "type" | "className"
> & {
  type: "primary" | "secondary";
};

function StyledButton({ type, ...allProps }: StyledButton) {
  return <Button className={`btn-${type}`} />;
}

必要的属性#

我们从类型定义中删除了一些道具,并将它们预设为合理的默认值。现在我们要确保我们的用户不会忘记设置一些道具。比如图片的alt属性或src 属性。

为此,我们创建了一个MakeRequired 辅助类型,删除了可选标志。

type MakeRequired<T, K extends keyof T> = Omit<T, K> &
  Required<{ [P in K]: T[P] }>;

然后用它来建立我们的道具。

type ImgProps 
  = MakeRequired<
    JSX.IntrinsicElements["img"], 
    "alt" | "src"
  >;

export function Img({ alt, ...allProps }: ImgProps) {
  return <img alt={alt} {...allProps} />;
}

const zz = <Img alt="..." src="..." />;

受控输入#

当你在React中使用常规的输入元素,并想预先填入数值时,之后你就不能再改变它们。这是因为value 属性现在是由React控制的。我们必须把value 在我们的状态中并控制它。通常情况下,只要用我们自己的类型与原始输入元素的道具相交就足够了。这是可选的,因为我们想在以后的组件中把它设置成一个默认的空字符串。

type ControlledProps = 
  JSX.IntrinsicElements["input"] & {
    value?: string;
  };

另外,我们也可以放弃旧的属性,重写它。

type ControlledProps =
  Omit<JSX.IntrinsicElements["input"], "value"> & {
    value?: string;
  };

并使用带有默认值的useState ,使其发挥作用。我们还将我们从原来的输入道具中传递的onChange 处理程序。

function Controlled({
  value = "", onChange, ...allProps
}: ControlledProps) {
  const [val, setVal] = useState(value);
  return (
    <input
      value={val}
      {...allProps}
      onChange={e => {
        setVal(() => e.target?.value);
        onChange && onChange(e);
      }}
    />
  );
}

有待扩展