# 使用 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' });