React & Typescript:组件的入门实例

React 组件的演化

组件复用方式 优势 劣势 状态
类组件(Class 发展时间长,接受度广泛 只能继承父类 作为一种传统开发模式,会长期存在
Mixin 可以复制任意对象的任意多个方法,实现组件间的复用 组件间相互依赖、耦合,可能产生冲突,不利于维护 被官方抛弃
高阶组件(HOC) 利用装饰器模式,在不改变组件的基础上,动态地为其添加新的能力 嵌套过多调试困难,需要遵循某些约定(不改变原始组件,透传 props 等) 能力强大,广泛引用
Hooks 替代类组件,多个 Hooks 间互不影响,避免嵌套地狱,开发效率高 切换新思维需要成本 React 的未来(官方主推)

函数组件

普通函数组件

上一篇中,我们创建了一个无状态组件(没有状态影响,作为纯静态展示) <Hello />,同时它也是一个函数组件。

import React from "react";
import { Button } from "antd";

interface Greeting {
  name: string;
  firstName: string;
  lastName: string;
}

const Hello = (props: Greeting) => <Button>hello {props.name}</Button>;

Hello.defaultProps = {
  firstName: "",
  lastName: "",
};

export default Hello;
复制代码

React.FC

在 react 的声明文件中,对函数组件单独定义了一个类型 — React.FC:

type React.FC<P = {}> = React.FunctionComponent<P>
复制代码

现在使用 React.FC 重新定义一下 <Hello />

const Hello: React.FC<Greeting> = ({ name, firstName, lastName }) => (
  <Button>hello {name}</Button>
);
复制代码

使用 React.FC 后的区别

FCFunctionComponent 的简写,这个类型定义了默认的 props (如 children)。

const Hello: React.FC<Greeting> = ({ name, firstName, lastName, children }) => (
  <Button>hello {name}</Button>
);
复制代码

在使用 React.FC后定义 defaultProps 时,默认属性必须是可选的(这和普通函数组件不同):

interface Greeting {
  name: string;
  firstName?: string;
  lastName?: string;
}

const Hello: React.FC<Greeting> = ({ name, firstName, lastName, children }) => (
  <div>
    <Button>hello {name}</Button>
  </div>
);

Hello.defaultProps = {
  firstName: "",
  lastName: "",
};
复制代码

小结

TypeScript 中,函数组件需要为 props 定义类型。

类组件

Component

类组件需要继承 Components 组件,在 react 的声明文件中,Component 被定义为泛型类:

// P: 属性的类型,默认{}
// S: 状态的类型,默认{}
// SS: snapshot
(alias) class Component<P = {}, S = {}, SS = any>
复制代码

实现

将上面的函数组件改造成类组件:

// src/componets/demo/HellpClass.tsx
import React, { Component } from "react";
import { Button } from "antd";

interface Greeting {
  name: string;
  firstName: string;
  lastName: string;
}

interface State {
  count: number;
}

class HelloClass extends Component<Greeting, State> {
  // 初始化 state
  state: State = { count: 0 };
  // 默认属性值
  static defaultProps = {
    firstName: "",
    lastName: "",
  };
  render() {
    return <Button>hello {this.props.name}</Button>;
  }
}

export default HelloClass;
复制代码

通过 setState<HelloClass /> 添加一个点击计数功能。

class HelloClass extends Component<Greeting, State> {
  state: State = { count: 0 };
  static defaultProps = {
    firstName: "",
    lastName: "",
  };
  render() {
    return (
      <>
        <div>您点击了 {this.state.count} 次</div>
        <Button
          onClick={() => {
            this.setState({ count: this.state.count + 1 });
          }}
        >
          hello {this.props.name}
        </Button>
      </>
    );
  }
}
复制代码

小结

TypeScript 中,类组件需要为 propsstate 定义类型。

高阶组件

我们现在要利用高阶组件包装一下 <HelloClass />,包装后的组件有一个新属性 loading,通过该属性控制被包装组件的 显示/隐藏。

React.ComponentType

指定被包装组件的类型为 React.ComponentType(一种 React 预定义类型),既可以是类组件,也可以是函数组件:

type React.ComponentType<P = {}> = React.ComponentClass<P, any> | React.FunctionComponent<P>
复制代码

实现

添加 <HelloHOC /> 组件:

import React, { Component } from "react";
import HelloClass from "./HelloClass";

interface Loading {
  loading: boolean;
}

/*
 ** WrapperComponetn: 需要被包装的组件
 */
function HelloHOC<P>(WrapperComponetn: React.ComponentType<P>) {
  // 定义 props 为 P 和 Loading 的交叉类型
  return class extends Component<P & Loading> {
    render() {
      // 解构 props,拆分出 loading
      const { loading, ...props } = this.props;
      // {...props}:属性透传
      return loading ? (
        <div>Loading...</div>
      ) : (
        <WrapperComponetn {...(props as P)} />
      );
    }
  };
}

// 导出经过高阶组件包装后的组件
export default HelloHOC(HelloClass);
复制代码

有个报错

我们在 index.tsx 中引入这个组件,这时会有一个报错:

import React from "react";
import ReactDOM from "react-dom";
import HelloHOC from "./components/demo/HelloHOC";

ReactDOM.render(
  <HelloHOC name="typescript" loading={true} />,
  document.querySelectorAll(".app")[0]
);

// ERROR! 因为 HelloClass 的静态属性 defaultProps 传不出来。
复制代码

解决方案:将 defaultProps 设置为可选属性。

interface Greeting {
  name: string;
  firstName?: string;
  lastName?: string;
}
复制代码

小结

TypeScript 中,高阶组件的使用会遇到很多类型问题,还有可能遇到一些已知的 bug,但这并不是高阶组件本身的问题,而是因为 react 声明文件没有很好的兼容。其实官方最推荐的是使用 Hooks,下面就再用 Hooks 实现一下吧

hooks

hooks 也是一种函数组件,对比类组件,明显简化了许多:

import React, { useEffect, useState } from "react";
import { Button } from "antd";

interface Greeting {
  name: string;
  firstName: string;
  lastName: string;
}

const HelloHooks = (props: Greeting) => {
  // 定义 [组件的状态,设置状态的方法],给定状态的初始值:不需要再定义类型
  const [count, setCount] = useState(0);
  const [text, setText] = useState<string | null>(null);
  return (
    <>
      <div>您点击了 {count} 次</div>
      <Button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        hello {props.name}
      </Button>
    </>
  );
};

HelloHooks.defaultProps = {
  firstName: "",
  lastName: "",
};

export default HelloHooks;
复制代码

利用 useEffect 新增一个功能: 点击超过 5 次给出提示。

const HelloHooks = (props: Greeting) => {
  const [count, setCount] = useState(0);
  const [text, setText] = useState<string | null>(null);

  // 只有当 count 改变时,渲染逻辑才会执行。
  useEffect(() => {
    if (count > 5) {
      setText("休息一下");
    }
  }, [count]);

  return (
    <>
      <div>
        您点击了 {count} 次,{text}
      </div>
      <Button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        hello {props.name}
      </Button>
    </>
  );
};
复制代码

为什么要定义为泛型?

不用泛型变量,this.props 的类型无法确定,在内部只能使用类型断言来访问属性:

(this.props as Greeting).name;
复制代码

这样很麻烦,而 React 声明文件把这些约束关系都用泛型定义好了。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享