React & Typescript:组件的入门实例

React 组件的演化

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

函数组件

普通函数组件

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

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:typescript

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)。markdown

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

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

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 定义类型。app

类组件

Component

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

// 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 /> 添加一个点击计数功能。post

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 定义类型。ui

高阶组件

咱们如今要利用高阶组件包装一下 <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 声明文件把这些约束关系都用泛型定义好了。

React & TS 系列

相关文章
相关标签/搜索