# 使用 FC 类型来声明函数组件

import React, { FC } from 'react';

/**
 * 声明 Props 类型
 */
export interface MyComponentProps {
  className?: string;
  style?: React.CSSProperties;
}

export const MyComponent: FC<MyComponentProps> = props => {
  return <div>hello react</div>;
};

也可以使用普通函数来声明:

export interface MyComponentProps {
  className?: string;
  style?: React.CSSProperties;
  // 手动声明 children
  children?: React.ReactNode;
}

export function MyComponent(props: MyComponentProps) {
  return <div>hello react</div>;
}

# 不要直接使用 export default 导出组件

这种方式导出的组件在 React Inspector 查看时会显示为 Unknown

export default (props: {}) => {
  return <div>hello react</div>;
};

使用命名 function 定义并导出:

export default function Foo(props: {}) {
  return <div>xxx</div>;
}

# 默认 props 声明

如果我们声明 props 有默认值:

import React, { FC } from 'react';

export interface HelloProps {
  name: string;
}

export const Hello: FC<HelloProps> = ({ name }) => <div>Hello {name}!</div>;

Hello.defaultProps = { name: 'TJ' };

// ❌! missing name
<Hello />;

可以这样子声明默认 props:

export interface HelloProps {
  name?: string; // 声明为可选属性
}

// 利用对象默认属性值语法
export const Hello: FC<HelloProps> = ({ name = 'TJ' }) => <div>Hello {name}!</div>;

如果非得使用 defaultProps, 可以如下代码. Typescript 可以推断和在函数上定义的属性, 这个特性在 Typescript 3.1开始支持.

import React, { PropsWithChildren } from 'react';

export interface HelloProps {
  name: string;
}

// 直接使用函数参数声明
// PropsWithChildren只是扩展了children, 完全可以自己声明
// type PropsWithChildren<P> = P & {
//    children?: ReactNode;
// }
const Hello = ({ name }: PropsWithChildren<HelloProps>) => <div>Hello {name}!</div>;

Hello.defaultProps = { name: 'TJ' };

// ✅ ok!
<Hello />;

这种方式也非常简洁, 只不过 defaultProps 的类型和组件本身的 props 没有关联性, 这会使得 defaultProps 无法得到类型约束, 所以必要时进一步显式声明 defaultProps 的类型: 注意这里的 Partial 是声明为可选属性。

Hello.defaultProps = { name: 'TJ' } as Partial<HelloProps>;

# 泛型函数组件

泛型在一些列表型或容器型的组件中比较常用, 直接使用FC无法满足需求:

import React from 'react';

export interface ListProps<T> {
  visible: boolean;
  list: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
}

export function List<T>(props: ListProps<T>) {
  return <div />;
}

// Test
function Test() {
  return (
    <List
      list={[1, 2, 3]}
      renderItem={i => {
        /*自动推断 i 为 number 类型*/
      }}
    />
  );
}

# Parent.Child 方式声明子组件

使用 Parent.Child 形式的 JSX 可以让节点父子关系更加直观, 它类似于一种命名空间的机制, 可以避免命名冲突. 相比 ParentChild 这种命名方式, Parent.Child 更为优雅些. 当然也有可能让代码变得啰嗦.

import React, { PropsWithChildren } from 'react';

export interface LayoutProps {}
export interface LayoutHeaderProps {} // 采用ParentChildProps形式命名
export interface LayoutFooterProps {}

export function Layout(props: PropsWithChildren<LayoutProps>) {
  return <div className="layout">{props.children}</div>;
}

// 作为父组件的属性
Layout.Header = (props: PropsWithChildren<LayoutHeaderProps>) => {
  return <div className="header">{props.children}</div>;
};

Layout.Footer = (props: PropsWithChildren<LayoutFooterProps>) => {
  return <div className="footer">{props.children}</div>;
};

// Test
<Layout>
  <Layout.Header>header</Layout.Header>
  <Layout.Footer>footer</Layout.Footer>
</Layout>;

# Forwarding Refs 通过 ref 调用组件方法

React.forwardRef 在 16.3 新增, 可以用于转发 ref, 适用于 HOC 和函数组件. 函数组件在 16.8.4 之前是不支持 ref 的, 配合 forwardRef 和 useImperativeHandle 可以让函数组件向外暴露方法

/*****************************
 * 
 * MyModal.tsx
 ****************************/
import React, { useState, useImperativeHandle, FC, useRef, useCallback } from 'react';

export interface MyModalProps {
  title?: React.ReactNode;
  onOk?: () => void;
  onCancel?: () => void;
}

/**
 * 暴露的方法, 使用`{ComponentName}Methods`形式命名
 */
export interface MyModalMethods {
  show(): void;
}

export const MyModal = React.forwardRef<MyModalMethods, MyModalProps>((props, ref) => {
  const [visible, setVisible] = useState();

  // 初始化ref暴露的方法
  useImperativeHandle(ref, () => ({
    show: () => setVisible(true),
  }));

  return <Modal visible={visible}>...</Modal>;
});

/*******************
 * Test.tsx
 *******************/
const Test: FC<{}> = props => {
  // 引用
  const modal = useRef<MyModalMethods | null>(null);
  const confirm = useCallback(() => {
    if (modal.current) {
      modal.current.show();
    }
  }, []);

  const handleOk = useCallback(() => {}, []);

  return (
    <div>
      <button onClick={confirm}>show</button>
      <MyModal ref={modal} onOk={handleOk} />
    </div>
  );
};

# 高阶组件

一个简单的高阶组件:

import React, { FC } from 'react';

/**
 * 声明注入的Props
 */
export interface ThemeProps {
  primary: string;
  secondary: string;
}

/**
 * 给指定组件注入'主题'
 */
export function withTheme<P>(Component: React.ComponentType<P & ThemeProps>) {
  /**
   * WithTheme 自己暴露的Props
   */
  interface OwnProps {}

  /**
   * 高阶组件的props, 忽略ThemeProps, 外部不需要传递这些属性
   */
  type WithThemeProps = P & OwnProps;

  /**
   * 高阶组件
   */
  const WithTheme = (props: WithThemeProps) => {
    // 假设theme从context中获取
    const fakeTheme: ThemeProps = {
      primary: 'red',
      secondary: 'blue',
    };
    return <Component {...fakeTheme} {...props} />;
  };

  WithTheme.displayName = `withTheme${Component.displayName}`;

  return WithTheme;
}

// Test
const Foo: FC<{ a: number } & ThemeProps> = props => <div style={{ color: props.primary }} />;
const FooWithTheme = withTheme(Foo);
() => {
  <FooWithTheme a={1} />;
};

# Render Props

React 的 props(包括 children)并没有限定类型, 它可以是一个函数. 于是就有了 render props, 这是和高阶组件一样常见的模式:

import React from 'react';

export interface ThemeConsumerProps {
  children: (theme: Theme) => React.ReactNode;
}

export const ThemeConsumer = (props: ThemeConsumerProps) => {
  const fakeTheme = { primary: 'red', secondary: 'blue' };
  return props.children(fakeTheme);
};

// Test
<ThemeConsumer>
  {({ primary }) => {
    return <div style={{ color: primary }} />;
  }}
</ThemeConsumer>;

# Context

Context 提供了一种跨组件间状态共享机制:

import React, { FC, useContext } from 'react';

export interface Theme {
  primary: string;
  secondary: string;
}

/**
 * 声明Context的类型, 以{Name}ContextValue命名
 */
export interface ThemeContextValue {
  theme: Theme;
  onThemeChange: (theme: Theme) => void;
}

/**
 * 创建Context, 并设置默认值, 以{Name}Context命名
 */
export const ThemeContext = React.createContext<ThemeContextValue>({
  theme: {
    primary: 'red',
    secondary: 'blue',
  },
  onThemeChange: noop,
});

/**
 * Provider, 以{Name}Provider命名
 */
export const ThemeProvider: FC<{ theme: Theme; onThemeChange: (theme: Theme) => void }> = props => {
  return (
    <ThemeContext.Provider value={{ theme: props.theme, onThemeChange: props.onThemeChange }}>
      {props.children}
    </ThemeContext.Provider>
  );
};

/**
 * 暴露hooks, 以use{Name}命名
 */
export function useTheme() {
  return useContext(ThemeContext);
}

# 使用 handleEvent 命名事件处理器

如果存在多个相同事件处理器, 则按照 handle{Type}{Event} 命名, 例如 handleNameChange.

export const EventDemo: FC<{}> = props => {
  const handleClick = useCallback<React.MouseEventHandler>(evt => {
    evt.preventDefault();
    // ...
  }, []);

  return <button onClick={handleClick} />;
};

# 事件处理器的类型定义

@types/react内置了以下事件处理器的类型:

type EventHandler<E extends SyntheticEvent<any>> = { bivarianceHack(event: E): void }['bivarianceHack'];
type ReactEventHandler<T = Element> = EventHandler<SyntheticEvent<T>>;
type ClipboardEventHandler<T = Element> = EventHandler<ClipboardEvent<T>>;
type CompositionEventHandler<T = Element> = EventHandler<CompositionEvent<T>>;
type DragEventHandler<T = Element> = EventHandler<DragEvent<T>>;
type FocusEventHandler<T = Element> = EventHandler<FocusEvent<T>>;
type FormEventHandler<T = Element> = EventHandler<FormEvent<T>>;
type ChangeEventHandler<T = Element> = EventHandler<ChangeEvent<T>>;
type KeyboardEventHandler<T = Element> = EventHandler<KeyboardEvent<T>>;
type MouseEventHandler<T = Element> = EventHandler<MouseEvent<T>>;
type TouchEventHandler<T = Element> = EventHandler<TouchEvent<T>>;
type PointerEventHandler<T = Element> = EventHandler<PointerEvent<T>>;
type UIEventHandler<T = Element> = EventHandler<UIEvent<T>>;
type WheelEventHandler<T = Element> = EventHandler<WheelEvent<T>>;
type AnimationEventHandler<T = Element> = EventHandler<AnimationEvent<T>>;
type TransitionEventHandler<T = Element> = EventHandler<TransitionEvent<T>>;

可以简洁地声明事件处理器类型:

import { ChangeEventHandler } from 'react';
export const EventDemo: FC<{}> = props => {
  /**
   * 可以限定具体Target的类型
   */
  const handleChange = useCallback<ChangeEventHandler<HTMLInputElement>>(evt => {
    console.log(evt.target.value);
  }, []);

  return <input onChange={handleChange} />;
};

# 自定义组件暴露事件处理器类型

主要注意命名规范:定义事件处理器类型以 {ComponentName}{Event}Handler 命名. 为了和原生事件处理器类型区分, 不使用 EventHandler 形式的后缀

import React, { FC, useState } from 'react';

export interface UploadValue {
  url: string;
  name: string;
  size: number;
}

/**
 * 暴露事件处理器类型
 */
export type UploadChangeHandler = (value?: UploadValue, file?: File) => void;

export interface UploadProps {
  value?: UploadValue;
  onChange?: UploadChangeHandler;
}

export const Upload: FC<UploadProps> = props => {
  return <div>...</div>;
};

# 获取原生元素 props 定义

有些场景我们希望原生元素扩展一下一些 props. 所有原生元素 props 都继承了 React.HTMLAttributes, 某些特殊元素也会扩展了自己的属性, 例如 InputHTMLAttributes. 具体可以参考 React.createElement 方法的实现:

import React, { FC } from 'react';

export function fixClass<
  T extends Element = HTMLDivElement,
  Attribute extends React.HTMLAttributes<T> = React.HTMLAttributes<T>
>(cls: string, type: keyof React.ReactHTML = 'div') {
  const FixedClassName: FC<Attribute> = props => {
    return React.createElement(type, { ...props, className: `${cls} ${props.className}` });
  };

  return FixedClassName;
}

/**
 * Test
 */
const Container = fixClass('card');
const Header = fixClass('card__header', 'header');
const Body = fixClass('card__body', 'main');
const Footer = fixClass('card__body', 'footer');

const Test = () => {
  return (
    <Container>
      <Header>header</Header>
      <Body>header</Body>
      <Footer>footer</Footer>
    </Container>
  );
};

# 为没有提供 Typescript 声明文件的第三方库自定义模块声明

可以在项目根目录下(和 tsconfig.json 同在一个目录下)放置一个global.d.ts. 放置项目的全局声明文件:

// /global.d.ts

// 自定义模块声明
declare module 'awesome-react-component' {
  // 依赖其他模块的声明文件
  import * as React from 'react';
  export const Foo: React.FC<{ a: number; b: string }>;
}

# 有状态组件和无状态组件

无状态组件内部不存储状态, 完全由外部的 props 来映射. 这类组件以函数组件形式存在, 作为低级/高复用的底层展示型组件. 无状态组件天然就是'纯组件', 如果无状态组件的映射需要一点成本, 可以使用 React.memo 包裹避免重复渲染。

# 一些特殊的 API:Exclude、Extract、Pick、Record、Omit、NonNullable、ReturnType

# Exclude<T,U> 从 T 中排除那些可以赋值给 U 的类型

从 T 中排除那些可以赋值给 U 的类型:

type Exclude<T, U> = T extends U ? never : T;

实例:

type T = Exclude<1|2|3|4|5, 3|4>  // T = 1|2|5 

此时 T 类型的值只可以为 1 、2 、5 ,当使用其他值是 TS 会进行错误提示。

Error:(8, 5) TS2322: Type '3' is not assignable to type '1 | 2 | 5'.

# Extract<T,U> 从 T 中提取那些可以赋值给 U 的类型

从 T 中提取那些可以赋值给 U 的类型。

type Extract<T, U> = T extends U ? T : never;

实例:

type T = Extract<1|2|3|4|5, 3|4>  // T = 3|4

此时T类型的值只可以为 3 、4 ,当使用其他值时 TS 会进行错误提示:

Error:(8, 5) TS2322: Type '5' is not assignable to type '3 | 4'.

# Pick<T,K> 从 T 中取出一系列 K 的属性(类似将所有属性设置为可选)

从 T 中取出一系列 K 的属性。

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

实例:

假如我们现在有一个类型其拥有 name 、 age 、 sex 属性,当我们想生成一个新的类型只支持 name 、age 时可以像下面这样:


interface Person {
  name: string,
  age: number,
  sex: string,
}
let person: Pick<Person, 'name' | 'age'> = {
  name: '小王',
  age: 21,
}

# Record<K,T> 将 K 中所有的属性的值转化为 T 类型

将 K 中所有的属性的值转化为 T 类型。

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

实例:

将 name、age 属性全部设为 string 类型。

let person: Record<'name' | 'age', string> = {
  name: '小王',
  age: '12',
}

# Omit<T,K> 从对象 T 中排除 key 是 K 的属性

从对象 T 中排除 key 是 K 的属性。

由于 TS 中没有内置,所以需要我们使用 Pick 和 Exclude 进行实现:

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>

实例:

排除 name 属性。

interface Person {
  name: string,
  age: number,
  sex: string,
}
let person: Omit<Person, 'name'> = {
  age: 1,
  sex: '男'
}

# NonNullable 排除 T 为 null 、undefined

排除 T 为 null 、undefined

type NonNullable<T> = T extends null | undefined ? never : T;

实例:

type T = NonNullable<string | string[] | null | undefined>; // string | string[]

# ReturnType 获取函数 T 返回值的类型

获取函数 T 返回值的类型:

type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;

infer R 相当于声明一个变量,接收传入函数的返回值类型。

实例:

type T1 = ReturnType<() => string>; // string
type T2 = ReturnType<(s: string) => void>; // void

# useState 初始化状态问题

大多数情况下,useState 的类型可以从初始化值推断出来。但当我们初始化值为 null、undefined 或者对象以及数组的时候,我们需要制定 useState 的类型。

// 可以推断 age 是 number类型
const [age, setAge] = useState(20);

// 初始化值为 null 或者 undefined时,需要显示指定 name 的类型
const [name, setName] = useState<string>();

// 初始化值为一个对象时
interface People {
    name: string;
    age: number;
    country?: string;
}
const [owner, setOwner] = useState<People>({name: 'rrd_fe', age: 5});

// 初始化值是一个数组时
const [members, setMembers] = useState<People[]>([]);

# useEffect

useEffect 用来在组件完成渲染之后增加副作用(side effect),可以返回一个函数,用来做一些状态还原、移除listener等 clean up的操作。不需要处理返回值,所以可以不指定他的类型。

useEffect(() => {
  const listener = addEventListener(name, callback)
  return () => {
    removeEventListener(listener)
  }
}, [name, callback])

# useMemo、useCallback

对于 useMemo 和 useCallback 我们可以从函数的返回值中推断出来他们返回的类型,需要显示指定。

const age = 12;
// 推断 doubleAge 是 number类型
const doubleAge = useMemo(() => {
    return age * 2;
}, [age]);

// 推断 addTen 类型是 (initValue: number) => number
const addTen = useCallback((initValue: number) => {
    return initValue + 10;
});

# useRef

useRef 有两种比较典型的使用场景: 场景一: 和 hook 之前的 ref 类似,用来关联一个 DOM 节点或者 class component 实例,从而可以直接操作 DOM 节点 或者 class component 的方法。通常会给 ref 的 readonly 属性 current 初始化为 null,直到 ref 关联到组件上。通常我们需要指定 useRef 的类型,参考如下:

const RRDTextInput = () => {
  const inputRef = useRef<TextInput>(null)
  return <TextInput ref={inputRef} placeholder="useRef" />
}

场景二:使用 ref 替代 class component 中的实例属性,这种场景我们可以从初始化值中推断出类型,current 也是可修改的。

// 推断 current 是 number 类型
const age = useRef(2);

# useReducer

useReducer 可以认为是简配版的 redux,可以让我们把复杂、散落在四处的 useState,setState 集中到 reducer 中统一处理。类似我们同样可以从 reducer 函数(state 逻辑处理函数)中推断出 useReducer 返回的 state 和 dispatch 的 action 类型,所以无需在显示的声明,参考如下实例:

type ReducerAction =
    | { type: 'switchToSmsLogin' | 'switchToAccountLogin' }
    | {
        type: 'changePwdAccount' | 'changeSmsAccount';
        payload: {
            actualAccount: string;
            displayAccount: string;
        };
    };

interface AccountState {
    loginWithPwd: boolean;
    pwdActualAccount: string;
    pwdDisplayAccount: string;
    smsActualAccount: string;
    smsDisplayAccount: string;
}

function loginReducer(loginState: AccountState, action: ReducerAction): AccountState {
    switch (action.type) {
        case 'switchToAccountLogin':
            return {
                ...loginState,
                pwdActualAccount: loginState.smsActualAccount,
                pwdDisplayAccount: loginState.smsDisplayAccount,
                loginWithPwd: !loginState.loginWithPwd,
            };
        // 密码登陆页账号发生变化
        case 'changePwdAccount':
            return {
                ...loginState,
                pwdActualAccount: action.payload.actualAccount,
                pwdDisplayAccount: action.payload.displayAccount,
            };
        default:
            return loginState;
    }
}

// 可以从 loginReducer 推断出
// loginState 的类型 满足 AccountState interface
// dispatchLogin 接受的参数满足 ReducerAction 类型
const [loginState, dispatchLogin] = useReducer(loginReducer, initialState);

dispatchLogin({ type: 'switchToAccountLogin' });
dispatchLogin({
    type: 'changePwdAccount',
    payload: {
        actualAccount,
        displayAccount,
    },
});

// 错误: 不能将 logout 类型赋值给 type
dispatchLogin({ type: 'logout' });
// 错误: { type: 'changePwdAccount' } 类型缺少 payload属性
dispatchLogin({ type: 'changePwdAccount' });

评 论:

更新: 11/21/2020, 7:00:56 PM