React + TypeScript 50 条规范和经验

TypeScript 代码整洁之道

注释

(1) 文件顶部的注释,包括描述、作者、日期

1
2
3
4
5
/**
* @description xxxxxx
* @author chengfeng
* @since 19/05/21
*/

(2) 模块的注释

1
2
3
4
5
6
/**
* 拷贝数据
* @param {*} data 要拷贝的源数据
* @param {boolean} [isDeep=false] 是否深拷贝,默认浅拷贝
* @return {*} 返回拷贝后的数据
*/

(3) 业务代码注释

1
/*业务代码注释*/

(4) 变量注释

1
2
3
4
5
6
7
8
interface IState {
// 名字
name: string;
// 电话
phone: number;
// 地址
address: string;
}

引用组件顺序

  • 先引用外部组件库,再引用当前组件块级组件, 然后是 common 里的公共函数库最后是 css 样式,加空行
1
2
3
4
5
6
import * as React from'react';
import { Dropdown, Menu, Icon } from'antd';
import Header from'./Header';
import toast from'common/toast';

import'./index.less';

引号

  • 使用单引号,或者 es6 的反引号

缩进

  • 使用两个空格
1
2
3
4
const handleCheck = () => {
onCancel && onCancel();
onClose && onClose();
};

分号

  • 除了代码块的以外的每个表达式后必须加分号。

括号

下列关键字后必须有大括号(即使代码块的内容只有一行):if, else, for, while, do, switch, try, catch, finally, with。

1
2
3
4
5
6
7
// not good
if (condition) doSomething();

// good
if (condition) {
doSomething();
}

空格

  • 二元和三元运算符两侧必须有一个空格,一元运算符与操作对象之间不允许有空格。
1
2
3
4
5
6
7
8
9
// bad
++ x;
y ++;
z = x?1:2;

// good
++x;
y++;
z = x ? 1 : 2;
  • 用作代码块起始的左花括号 { 前必须有一个空格。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// bad
if (condition){
}

while (condition){
}

function funcName(){
}

// good
if (condition) {
}

while (condition) {
}

function funcName() {
}
  • if / else / for / while / function / switch / do / try / catch / finally 关键字后,必须有一个空格。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// bad
if(condition) {
}

while(condition) {
}

(function() {
})();

// good
if (condition) {
}

while (condition) {
}

(function () {

})();
  • 在对象创建时,属性中的 : 之后必须有空格,: 之前不允许有空格。
1
2
3
4
5
6
7
8
9
10
11
12
13
// bad
var obj = {
a : 1,
b:2,
c :3
};

// good
var obj = {
a: 1,
b: 2,
c: 3
};

换行

  • 每个独立语句结束后必须换行。
  • 在函数声明、函数表达式、函数调用、对象创建、数组创建、for 语句等场景中,不允许在 , 或 ; 前换行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// bad
var obj = {
a: 1
, b: 2
, c: 3,
};

function test()
{
...
}
for (const key in object)
{
if (object.hasOwnProperty(key)) {
const element = object[key];

}
}
// good
var obj = {
a: 1,
b: 2,
c: 3,
};

function test() {
...
}

for (const key in object) {
if (object.hasOwnProperty(key)) {
const element = object[key];

}
}
  • 下列关键字后:else, catch, finally 不需要换行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// bad
if (condition) {
...
}
else {
...
}

try {
...
}
catch (e) {
...
}
finally {
...
}


// good
if (condition) {
...
} else {
...
}

try {
...
} catch (e) {
...
} finally {
...
}

数组、对象

  • 对象属性名不需要加引号;
  • 对象以缩进的形式书写,不要写在一行;
  • 数组最后不要有逗号。
  • 对象最后要有逗号。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

// bad
const a = {
'b': 1
};

const a = {b: 1};

const a = {
b: 1,
c: 2
};
const arr = [1, 2, 3, 4,];

// good
const a = {
b: 1,
c: 2,
};

const arr = [1, 2, 3, 4];

命名

  • 类名: 大驼峰式风格,字母和数字,例如:AbcTest。禁止汉字、特殊符号,禁止非大驼峰式风格。
  • 函数名: 小驼峰式风格,字母和数字,例如:abcTest。禁止汉字、特殊符号,禁止非小驼峰式风格,例如snake_case等。
  • 变量名: 同函数名。
  • 常量: 全大写风格,大写字母、数字和下划线,单词之间以下划线分隔,例如:ABC_TEST。禁止汉字、特殊符号、小写字母。
  • 使用 onXxx 形式作为 props 中用于回调的属性名称。
1
2
3
4
interface IProps {
onClose?: () =>void;
onOk?: (item: Record<string, any>) =>void;
}
  • 组件内的事件函数使用 handle 开头尾,handleCheckBtn。
  • 使用 withXxx 形式的词作为高阶组件的名称。
  • 接口命名前面带上 I 表示 interface
1
2
interface IProps {}
interface IState {}

类型断言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// bad
function getLength(something: string | number): number {
return something.length;
}

// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
// Property 'length' does not exist on type 'number'.

// bad
function getLength(something: string | number): number {
if ((<string>something).length) {
return (<string>something).length;
} else {
return something.toString().length;
}
}

// good
function getLength(something: string | number): number {
if (typeof something === 'string') {
return something.length;
} else {
return something.toString().length;
}
}

interface声明顺序

日常用到比较多的是四种,只读参数放第一位,必选参数第二位,可选参数次之,不确定参数放最后

1
2
3
4
5
6
7
8
interface iProps {
readonly x: number;
readonly y: number;
name: string;
age: number;
height?: number;
[propName: string]: any;
}

ts好用的相关工具泛型

  • Record<string,any> 用这个来声明对象结构的类型
1
2
3
4
5
// 用于定义一个javascript的对象,key是字符串,value是任意类型
const people:Record<string,any> = {
name: 'chengfeng',
age: 10
}
  • Partial 作用是将传入的属性变为可选项.
1
2
3
4
5
6
7
8
9
interface iPeople {
title: string;
name: string;
}

const people: Partial<Todo> = {
title: 'Delete inactive users',
};
//定义的结构可以是接口iPeople的任意key
  • Readonly 作用是将传入的属性变为变成只读
1
2
3
4
5
6
7
8
9
10
interface iPeople {
title: string;
name: string;
}

const people: Readonly<Todo> = {
title: 'todo list',
name: chenfeng;
};
title name属性就是只读的了
  • Required 的作用是将传入的属性变为必选项
1
2
3
4
5
6
7
8
interface iPeople {
title?: string;
name?: string;
}

const people1: Props = { title: 'ts' }; // OK

const people22: Required<iPeople> = { title: 'ts' }; // Error: property 'name' missing

查看更多

ts一些好用的小tips

  • keyof
1
2
3
4
5
6
interface iPeople {
name: string;
age: number
}

type T = keyof iPeople // -> "name" | "age"
  • in
1
2
3
4
5
6
type Keys = "a" | "b"
type Obj = {
[p in Keys]: any
}

// -> { a: any, b: any }

规范其他

  • 不要使用 var 声明变量
  • 不会被修改的变量使用 const 声明
  • 去除声明但未被引用的代码
  • 禁止在代码里使用 debug
  • 不允许有空的代码块

仅当初始 state 需要从 props 计算得到的时候,才将 state 的声明放在构造函数中,其它情况下使用静态属性声明 state,并且一般情况下不要将 prop 传给 state,

1
2
3
4
5
6
7
8
9
// bad
constructor (){
this.setState({ people: this.props.people })
}

// good
state: IState = {
people: {},
};

渲染默认值

  • 添加非空判断可以提高代码的稳健性,例如后端返回的一些值,可能会出现不存在的情况,应该要给默认值.
1
2
3
4
5
6
7
8
9
// bad
render(){
{name}
}

// good
render(){
{name || ''}
}
  • 还有一种情况,就是本来后端应该返回一个数组给你,但是数据库取不到数据,可能后端给你返回了null,然后前端null.length。这样就gg了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// bad
const { list, totalCount } = await getPeopleList(keyword, page, pageSize);
//list 可能是null或者undefined
//list.length将直接导致前端报错

this.setState({
status: STATUS.READY,
apps: list,
total: totalCount,
page: page,
});


// good const { list, totalCount } = await getPeopleList(keyword, page, pageSize);
this.setState({
status: STATUS.READY,
apps: list || [],
total: totalCount || 0,
page: page,
});

不确定的属性,最后却疯狂的用…访问不存在的属性

例如一些地方,不确定这个变量里面到底有什么,但自己觉得有,就疯狂的…,最明显的就是后端返回了一个对象给你,前端拿到之后判断都不判断直接data.dataList.forEach()

1
2
3
4
5
6
7
8
9
// bad
const data = await getPeopleList(keyword, page, pageSize);
data.dataList.forEach() // 直接挂了

// good
const data = await getPeopleList(keyword, page, pageSize);
if (data && data.dataList && Array.isArray(data.dataList) {
data.dataList.forEach()
}

数据格式转换

  • 把字符串转整型可以使用+号
1
2
let maxPrice = +form.maxPrice.value;
let maxPrice = Number(form.maxPrice.value);
  • 转成 boolean 值用!!
1
let mobile = !!ua.match(/iPhone|iPad|Android|iPod|Windows Phone/);

判断条件真假

js 中以下为假,其他情况为真

  • false
  • null
  • undefined
  • 0
  • ‘’ (空字符串)
  • NaN

简单组件可以使用函数代替

1
2
3
4
5
6
7
8
9
10
11
// bad
class Listing extends React.Component{
render() {
return<div>{this.props.hello}</div>;
}
}

// good
function Listing({ hello }) {
return<div>{hello}</div>;
}

对于常用的属性进行缓存

1
2
3
4
5
6
7
8
// bad
this.props.app.openid;
this.state.time

// good
const { app } = this.props;
const { time } = this.state;
console.log(app.openid)

input 输入框使用 trim()

1
2
3
4
5
// bad
let searchContent = form.search.value;

// good
let searchContent = form.search.value.trim();

使用 location 跳转前需要先转义

1
2
3
4
5
// bad
window.location.href = redirectUrl + '?a=10&b=20';

// good
window.location.href = redirectUrl + encodeURIComponent('?a=10&b=20');

使用 react-router

1
2
3
4
5
6
7
8
9
10
11
12
13
// bad
import { withRouter, RouteComponentProps } from'react-router-dom';

export interface IProps extends RouteComponentProps<any> {}
class App extends React.Component<IProps, AppStates> {}
export default withRouter(App);


// good
import { withRouter, RouteComponentProps } from'react-router-dom';

class App extends React.Component<IProps & RouteComponentProps<{}>, AppStates> {}
export default withRouter(App);

同时开发,数据请求 api 目录 git 冲突目录方案

  • 在 api 目录下新建一个目录,目录对应一级 tab,这个目录内放置一个 index.js ,最后把二级 tab 组件所使用的 api 请求都在这个 index.js 内引入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 目前

|- api
|- pageA.ts
|- pageB.ts

// 建议

|- api
|- pageA
|- index.js
|- aaa.js
|- bbb.js
|- pageB
|- index.js
|- aaa.js
|- bbb.js
|- ccc.js

组件嵌套过深

  • 组件一般不要超过三层,最多四层,层级过深可能会导致数据传递过深,在做一些颗粒度比较细的操作的时候,处理起来较为繁琐,可以使用 redux 等状态管理工具替代。

代码过滤掉你没考虑到的情况

  • 例如一个函数,你只想操作字符串,那你必须在函数开头就只允许参数是字符串
1
2
3
4
5
function parse (str:string){
if (typeof(str) === 'string' ) {

}
}

业务代码里面的异步请求需要 try catch

  • ajax 请求,使用 try catch,错误提示后端返回,并且做一些失败后的状态操作例如进入列表页,我们需要一个 loading 状态,然后去请求数据,可是失败之后,也需要把 loading 状态去掉,把 loading 隐藏的代码就写在 finally 里面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
getStudentList = async () => {
try {
this.setState({
loading: true,
isEmpty: false
});
await getStudentList({});
this.setState({
loading: false,
isEmpty: true
});
} catch (e) {
// TODO
console.log(e)
} finally {
// 失败之后的一些兜底操作
this.setState({
loading: false,
isEmpty: true
});
}
};

setState有三种用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 对象
this.setState({

})

// 函数,一般是用于在setState之前做一些操作
this.setState(() => {
// TODO
console.log('')
return {
a:300
}
}
)

// 第二个参数,一般是用于在setState之后做一些操作
this.setState({
a:300
}, () => {
// TODO
})

setState可能是同步的

  • setState 在react里的合成事件和钩子函数中是“异步”的。
  • setState 在原生事件和 setTimeout 中是同步的。

不要在 setState 前面加 await

  • setState 前面也是可以带 await 的,会变成同步设置状态,但这是一种巧合,不确定未来哪个版本就不支持了,为了遵循 react 框架的设计原则,我们使用回掉函数的形式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// bad
func = async (name, value, status) => {
await this.setState({
name
});
// TODO
};

// good
func = (name, value, status) => {
this.setState(
{
name
},
() => {
// TODO
}
);
};

阻止事件默认行为

  • 在 React 中你不能通过返回 false 来阻止默认行为。必须明确调用 preventDefault 。

在 componentWillUnmount 里面去除副作用的函数

  • 清除 EventListener
  • 中止数据请求
  • 清除定时器

key

  • 对于组件中的 key 优化,起到最大化重用 dom
1
2
3
4
5
6
7
//bad
this.state.dataAry.map((item, index) => {
return <span key={index} />;
});

//good
this.state.dataAry.map(item => <span key={item.id} />);

for-in 中一定要有 hasOwnProperty 的判断(即禁止直接读取原型对象的属性)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//bad
const arr = [];
const key = '';

for (key in obj) {
arr.push(obj[key]);
}

//good
const arr = [];
const key = '';

for (key in obj) {
if (obj.hasOwnProperty(key)) {
arr.push(obj[key]);
}
}

第三方库函数的使用

  • 用 try catch 包裹,防止第三方库的出现错误,导致整个程序崩溃
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*
* Echart 用于代绘制图表,但当其自身发生错误时,可能影响到业务代码的执行
*/
// bad
const iniDom = document.getElementById('init-container');
const echartObj = echarts.init(iniDom);
this.setState(
{
echartObj
},
() => {
const { echartObj } = this.state;
// 更新图表
echartObj.setOption(CHART_CONFIG, true);
}
);

// good
try {
const iniDom = document.getElementById('init-container');
const echartObj = echarts.init(iniDom);
this.setState(
{
echartObj
},
() => {
const { echartObj } = this.state;
// 更新图表
echartObj.setOption(CHART_CONFIG, true);
}
);
} catch (error) {
// TODO
}

防止 xss 攻击

  • input,textarea 等标签,不要直接把 html 文本直接渲染在页面上,使用 xssb 等过滤之后再输出到标签上;
1
2
3
4
5
6
7
8
import { html2text } from'xss';
render(){
<div
dangerouslySetInnerHTML={{
__html: html2text(htmlContent)
}}
/>
}

在组件中获取真实 dom

  • 使用 16 版本后的 createRef()函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyComponent extends React.Component<iProps, iState> {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}

render() {
return<inputtype="text"ref={this.inputRef} />;
}

componentDidMount() {
this.inputRef.current.focus();
}
}

减少魔法数字

  • 写代码的时候尽量减少一些未知含义的数字,尽量用英文单词。例如type === 0的时候做了一些操作,让人不知所以然。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// bad
if (type !== 0) {
// TODO
}

// good
const STATUS: Record<string, any> = {
READY: 0,
FETCHING: 1,
FAILED: 2
};

if (type === STATUS.READY) {
// TODO
}

// best
enum STATUS {
// 就绪
READY = 0,
// 请求中
FETCHING = 1,
// 请求失败
FAILED = 2,
}

如果需要优化 react 性能(一般用不到)

  • 如果组件的 state 和 props 都是简单类型,可以继承 PureComponent 而不是 Component
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Component, PureComponent } from'react';
// bad
class Message extends Component{
render() {
return<span>{this.state.message}</span>;
}
}

// good
class Message extends PureComponent{
render() {
return<span>{this.state.message}</span>;
}
}
  • 重写 shouldComponentUpdate 方法,在 shouldComponentUpdate 里面根据 state,props 是否有改变来判断是否需要重新渲染.如果组件继承了 PureComponent 就没必要再重写 shouldComponentUpdate 方法
1
2
3
4
5
6
7
import { isReactPropsEqual, isReactStateEqual } from'@fe/common/lib/equal';
shouldComponentUpdate(nextProps:IProps, nextState:IState) {
if (isReactStateEqual(nextState,this.state) && isReactPropsEqual(nextProps,this.props)) {
return false;
}
return true;
}

Event 事件对象类型

很多小伙伴用了很久的ts,都不知道常用 Event 事件对象类型:

ClipboardEvent 剪贴板事件对象

DragEvent 拖拽事件对象

ChangeEvent Change 事件对象

KeyboardEvent 键盘事件对象

MouseEvent 鼠标事件对象

TouchEvent 触摸事件对象

WheelEvent 滚轮事件对象

AnimationEvent 动画事件对象

TransitionEvent 过渡事件对象

1
2
3
4
5
import { MouseEvent } from'react';

interface IProps {
onClick(event: MouseEvent<HTMLDivElement>): void;
}

使用私有属性取代state状态

对于一些不需要控制ui的状态属性,我们可以直接绑到this上, 即私有属性,没有必要弄到this.state上,不然会触发渲染机制,造成性能浪费
例如请求翻页数据的时候,我们都会有个变量。

1
2
3
4
5
6
7
8
9
10
11
// bad
state: IState = {
pageNo:1,
pageSize:10
};

// good
queryParams:Record<string,any> = {
pageNo:1,
pageSize:10
}

代码细粒度的思考

总结四句话。我们在写组件或者函数的的时候,工具函数和业务逻辑抽离,表单校验和业务抽离、事件函数和业务抽离,ajax和业务抽离。
例如有些页面是通过location.href跳转的,我们有些业务逻辑等都是放到didmountMount,但是后期改需求,可能要用react-router进行跳转,可能要改的逻辑就会很多了,所以函数抽离出来,需求更新就少改一点代码。
如果还不确定如何划分函数的细粒度,我有个建议。使用过两次以上的代码,要抽离组件或者函数,两次的可以不用

if else 等判断太多了,后期难以维护。

个人觉得if else 嵌套深看起来也不会太难受,难受的是,项目迭代久之后,自己都忘记曾经写过这些代码,而且类型多或者不确定有什么类型,是否后期还会加的情况下,改起来就非常复杂了,而且很容易踩坑和背锅。
用配置取代if嵌套,大概就是抽离一个config.ts出来,里面放一些配置。

例如你的业务代码里面,会根据不同url参数,代码会执行不同的逻辑.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//info?type=wechat&uid=123456&

const qsObj = qs(window.location.url)
const urlType = qsObj.type
// bad
if (urlType === 'wechat') {
doSomeThing()
} elseif () {
doSomeThing()
} elseif () {
doSomeThing()
} elseif () {
doSomeThing()
}

// good
config.t
const urlTypeConfig: Record<string, typeItem> = {
'wechat': { // key 就是对应的type
name: 'wechat',
show: ['header', 'footer', 'wechat'] // 展示什么,可能是异步的
pession: ['admin'], // 权限是什么,可能是异步的
},
'zhifubao': { // key 就是对应的type
name: 'zhifubao',
show: ['header', 'footer', 'zhifubao'] // 展示什么,可能是异步的
pession: ['admin'], // 权限是什么,可能是异步的
},
}

// 业务逻辑
const qsObj = qs(window.location.url)
const urlType = qsObj.type
urlTypeConfig.forEach(item => {
if(urlType === item.type) {
doSomeThing(item.show)
}
})

不要使用renderXXX,要使用函数式组件

发现团队一些小伙伴为了减少render函数里面的代码量,会把一些元素拆分到函数里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// bad
renderHeader = () => {
return (<div />)
}
renderBody = () => {
return (<div />)
}
renderFooter = () => {
return (<div />)
}
render(){
return(
<div>
renderHeader()
renderBody()
renderFooter()
</div>
)
}

更好的办法,是用函数式组件取代在当前组件里面写方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// good
function RenderHeader(props) = {
return (<div />)
}
function RenderBody(props) = {
return (<div />)
}
function RenderFooter(props) = {
return (<div />)
}
class Component extends React.Component<iProps, iState>{
render () {
return(
<div>
<RenderHeader />
<RenderBody />
<RenderFooter />
</div>
)
}
}

a标签安全问题

使用a标签打开一个新窗口过程中的安全问题。新页面中可以使用window.opener来控制原始页面。如果新老页面同域,那么在新页面中可以任意操作原始页面。如果是不同域,新页面中依然可以通过window.opener.location,访问到原始页面的location对象

在带有target=”_blank”的a标签中,加上rel=”noopener”属性。如果使用window.open的方式打开页面,将opener对象置为空。

1
2
var newWindow = window.open();
newWindow.opener = null;

void 0 替代undefined

1
2
3
4
5
6
clearSessioin = () => {

req.session.userName = undefined;

req.session.userName = void 0
}

前端不要操作cookie

在做一些前后端鉴权的时候,后端应该开启domain,secure,httponly严格模式,禁止前端操作cookie,防止csrf攻击。

代码检查插件

我们可以使用构建工具继承 husky eslint tslint lint-stage prettier来规范代码。

React组件绑定this的四种方式

用react进行开发组件时,我们需要关注一下组件内部方法this的指向,react定义组件的方式有两种,一种为函数组件,一种为类组件,类组件内部可以定义一些方法,
这些方法的this需要绑定到组件实例上, 一共有四种方案:

  • 第一种方案,在构造函数内部使用bind绑定this,这样做的好处是,避免每次渲染时都要重新绑定,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, {Component} from 'react'

class Test extends React.Component {
constructor (props) {
super(props)
this.state = {message: 'Allo!'}
this.handleClick = this.handleClick.bind(this)
}

handleClick (e) {
console.log(this.state.message)
}

render () {
return (
<div>
<button onClick={ this.handleClick }>Say Hello</button>
</div>
)
}
}
  • 第二种方案同样是用bind,但是这次不再构造函数内部使用,而是在render函数内绑定,但是这样的话,每次渲染都需要重新绑定,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, {Component} from 'react'

class Test extends React.Component {
constructor (props) {
super(props)
this.state = {message: 'Allo!'}
}

handleClick (name, e) {
console.log(this.state.message + name)
}

render () {
return (
<div>
<button onClick={ this.handleClick.bind(this, '赵四') }>Say Hello</button>
</div>
)
}
}
  • 第三种方案是在render函数中,调用方法的位置包裹一层箭头函数,因为箭头函数的this指向箭头函数定义的时候其所处作用域的this,而箭头函数在render函数中定义,render函数this始终指向组件实例,所以箭头函数的this也指向组件实例,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Test extends React.Component {
constructor (props) {
super(props)
this.state = {message: 'Allo!'}
}

handleClick (e) {
console.log(this.state.message)
}

render () {
return (
<div>
<button onClick={ ()=>{ this.handleClick() } }>Say Hello</button>
</div>
)
}

以上这种方式有个小问题,因为箭头函数总是匿名的,如果你打算移除监听事件,是做不到的,那么怎么做才可以移除呢?看下一种方案。

  • 第四种方案,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Test extends React.Component {
constructor (props) {
super(props)
this.state = {message: 'Allo!'}
}

handleClick = (e) => {
console.log(this.state.message)
}

render () {
return (
<div>
<button onClick={ this.handleClick }>Say Hello</button>
</div>
)
}
}

不过,在Classes中直接赋值是ES7的写法,ES6并不支持,你有两种选择,一种是配置你的开发环境支持ES7,一种使采用如下方式,下面这种方式是第四种方案的另外一种写法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Test extends React.Component {
constructor (props) {
super(props)
this.state = {message: 'Allo!'}
this.handleClick = (e) => {
console.log(this.state.message)
}
}

render () {
return (
<div>
<button onClick={ this.handleClick }>Say Hello</button>
</div>
)
}

React中关于children和render props

组件的创建方式

  • 使用createClass方式创建(已经被淘汰了)

  • 类组件

1
2
3
4
5
6
import React, { Component } from'react'
export default class Components1 extends Component{
render() {
return<div />
}
}
  • 函数组件(比较推荐的方式)
1
2
3
4
5
6
7
8
9
10
11
12
import React from'react'
function Child(props) {
return <></>
}

export default () => {
return (
<>
<Child />
</>
)
}

组件的调用方式(使用children)

直接把组件当做一个DOM节点,在里面写内容,在该组件中使用childrend进行渲染

  • 简单的引用
1
2
3
4
5
6
7
8
9
10
11
import React from'react'
function Child(props) {
return <>我是child组件</>
}
export default () => {
return (
<>
<Child />
</>
)
}
  • 组件中传递html代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from'react'
function Child(props) {
console.log(props)
return (
<>
<p>我是child组件</p>
{props.children}
</>
)
}

export default () => {
return (
<>
<Child>
{/* 在组件中直接写内容可以传递到该组件的children上 */}
<h1>我是父组件传递进去的</h1>
<h2>我是父组件传递进去的内容二</h2>
</Child>
</>
)
}
  • 传递一个组件进去
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React from'react'
function Parent(props) {
return (
<>
<p>我是Parent组件</p>
{props.children}
</>
)
}
function Child(props) {
return (
<>
<p>我是Child组件</p>
</>
)
}
export default () => {
return (
<>
<Parent>
<Child />
</Parent>
</>
)
}
  • 组件中传递一个函数进去
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from'react'
function Child(props) {
return (
<>
<p>我是child组件</p>
{props.children({ name: '哈哈', gender: '男' })}
</>
)
}
export default () => {
return (
<>
<Child>
{args => {
console.log(args)
return <divid="child">{args.name}</div>
}}
</Child>
</>
)
}

render props的使用参考文档

主要作用点

  • 1、使用Render Props解决来横切关注点(组件的复用[复用组件内部一些逻辑])
  • 2、Render prop 是一个用于告知组件需要渲染什么内容的函数 prop与传统组件的props有点类似,只是希望渲染的是一个组件或者一个DOM节点
  • 3、Render props主要用于组件代码的复用

代码案例

  • 使用render props渲染一个DOM节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React from'react'
export default function Render03() {
return (
<div>
<Child
render={props => {
// 可以接收render函数里面的参数
console.log(props)
return (
<divstyle={{color: '#f90' }}>
我是渲染出来的--{props.name}--{props.gender}
</div>
)
}}
/>
</div>
)
}

function Child(props) {
return (
<>
<h1>我是child组件</h1>
{/* render里面传递参数,真正渲染的地方接收参数 */}
{props.render({ name: '张三', gender: '男' })}
</>
)
}
  • 使用render props渲染一个组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React from'react'
export default function Render04() {
return (
<div id="render04">
<Child1 render={ props => <Child2 {...props} />} />
</div>
)
}

function Child1(props) {
return (
<>
<h2>我是child1组件</h2>
{props.render({ name: '张三', gender: '男' })}
</>
)
}

function Child2(props) {
console.log(props)
return (
<>
<h2>我是child2组件</h2>
<h3>{props.name}</h3>
</>
)
}
  • 使用render props达到组件的复用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import React, { useState } from'react'
export default function Render05() {
return (
<div>
<Mouse
render={props => {
return <Cat {...props} />
}}
/>
<Mouse
render={props => {
console.log(props)
return (
<div
style={{
width: '100px',
height: '100px',
background: '#f90',
position: 'absolute',
top: props.y - 50,
left: props.x - 50
}}
/>
)
}}
/>
</div>
)
}

function Cat(props) {
return (
<div>
<div
style={{
position: 'absolute',
width: '100px',
height: '100px',
background: '#f00',
cursor: 'move',
left: props.x - 50,
top: props.y - 50
}}
/>
</div>
)
}
function Mouse(props) {
const [location, setLocation] = useState({ x: 0, y: 0 })

const handleMouseMove = event => {
setLocation({
x: event.clientX,
y: event.clientY
})
}
return (
<div style={{ height: '100%' }} onMouseMove={handleMouseMove}>
{props.render(location)}
</div>
)
}

什么是错误边界

在 React 中,我们通常有一个组件树。如果任何一个组件发生错误,它将破坏整个组件树。没有办法捕捉这些错误,我们可以用错误边界优雅地处理这些错误。
错误边界有两个作用

  • 如果发生错误,显示回退UI
  • 记录错误

下面是ErrorBoundary类的一个例子。如果类实现了 getDerivedStateFromError或componentDidCatch 这两个生命周期方法的任何一下,
那么这个类就会成为ErrorBoundary。
前者返回{hasError: true}来呈现回退UI,后者用于记录错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React from 'react'

export class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}

componentDidCatch(error, info) {
// You can also log the error to an error reporting service
console.log('Error::::', error);
}

render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>OOPS!. WE ARE LOOKING INTO IT.</h1>;
}

return this.props.children;
}
}

以下是我们如何在其中一个组件中使用ErrorBoundary。使用ErrorBoundary类包裹 ToDoForm和ToDoList。 如果这些组件中发生任何错误,我们会记录错误并显示回退UI。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react';
import '../App.css';
import { ToDoForm } from './todoform';
import { ToDolist } from './todolist';
import { ErrorBoundary } from '../errorboundary';

export class Dashboard extends React.Component {

render() {
return (
<div className="dashboard">
<ErrorBoundary>
<ToDoForm />
<ToDolist />
</ErrorBoundary>
</div>
);
}
}

什么是 Fragments

在React中,我们需要有一个父元素,同时从组件返回React元素。有时在DOM中添加额外的节点会很烦人。使用 Fragments,我们不需要在DOM中添加额外的节点。我们只需要用 React.Fragment 或才简写 <> 来包裹内容就行了。如下 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Without Fragments   
return (
<div>
<CompoentA />
<CompoentB />
<CompoentC />
</div>
)

// With Fragments
return (
<React.Fragment>
<CompoentA />
<CompoentB />
<CompoentC />
</React.Fragment>
)

// shorthand notation Fragments
return (
<>
<CompoentA />
<CompoentB />
<CompoentC />
</>
)

什么是传送门(Portals)

默认情况下,所有子组件都在UI上呈现,具体取决于组件层次结构。Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。

这里有一个例子。默认情况下,父组件在DOM层次结构中有子组件。

我们可以将 children 组件移出parent 组件并将其附加 id 为 someid 的 Dom 节点下。
首先,获取 id 为 someid,我们在constrcutorand中创建一个元素div,将child附加到componentDidMount中的someRoot。 最后,我们在ReactDOM.createPortal(this.props.childen),domnode的帮助下将子节点传递给该特定DOM节点。
首先,先获取 id 为someid DOM元素,接着在构造函数中创建一个元素div,在 componentDidMount方法中将 someRoot 放到 div 中 。 最后,通过
ReactDOM.createPortal(this.props.childen), domnode)将 children 传递到对应的节点下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const someRoot = document.getElementById('someid');

class Modal extends React.Component {
constructor(props) {
super(props);
this.el = document.createElement('div');
}

componentDidMount() {
someRoot.appendChild(this.el);
}

componentWillUnmount() {
someRoot.removeChild(this.el);
}

render() {
return ReactDOM.createPortal(
this.props.children,
this.el,
);
}
}

你要的 React 面试知识点,都在这了


React - 关键类型

  • 展示性组件(FunctionComponent

React.FunctionComponent<P> or React.FC<P>

const MyComponent: React.FC<Props> = ...

  • 有状态组件(ClassComponent

React.Component<P, S>

class MyComponent extends React.Component<Props, State> { ...

  • 组件Props

React.ComponentProps<typeof Component>

获取组件(适用于ClassComponent、FunctionComponent)的Props的类型

type MyComponentProps = React.ComponentProps<typeof MyComponent>;

  • React.FC | React.Component的联合类型

React.ComponentType<P>

1
2
3
const withState = <P extends WrappedComponentProps>(
WrappedComponent: React.ComponentType<P>,
) => { ...
  • React 要素

React.ReactElement<P> or JSX.Element

表示React元素概念的类型 - DOM组件(例如)或用户定义的复合组件()

const elementOnly: React.ReactElement = <div /> || <MyComponent />;

  • React Node

React.ReactNode

表示任何类型的React节点(基本上是ReactElement(包括Fragments和Portals)+ 原始JS类型 的合集)

1
2
const elementOrPrimitive: React.ReactNode = 'string' || 0 || false || null || undefined || <div /> || <MyComponent />;
const Component = ({ children: React.ReactNode }) => ...
  • React CSS属性

React.CSSProperties

代表着Style Object在 JSX 文件中(通常用于 css-in-js)

1
2
const styles: React.CSSProperties = { flexDirection: 'row', ...
const element = <div style={styles} ...

  • 通用的 React Event Handler

React.ReactEventHandler<HTMLElement>

1
2
3
const handleChange: React.ReactEventHandler<HTMLInputElement> = (ev) => { ... } 

<input onChange={handleChange} ... />
  • 特殊的 React Event Handler

React.MouseEvent<E> | React.KeyboardEvent<E> | React.TouchEvent<E>

1
2
3
const handleChange = (ev: React.MouseEvent<HTMLDivElement>) => { ... }

<div onMouseMove={handleChange} ... />

React 组件模式

  • Function Components - FC 纯函数组件(无状态)

顾名思义,纯函数组件本身不具备 State,所以没有状态,一切通过 Props

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { FC, ReactElement, MouseEvent  } from'react'

type Props = {
label: string,
children: ReactElement,
onClick?: (e: MouseEvent<HTMLButtonElement>) =>void
}

const FunctionComponent: FC<Props> = ({ label, children, onClick }: Props) => {
return (
<div>
<span>
{label}:
</span>
<button type="button" onClick={onClick}>
{children}
</button>
</div>
)
}

export default FunctionComponent

扩展属性(spread attributes)

利用 ... 对剩余属性进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { FC, ReactElement, MouseEvent, CSSProperties } from'react'

type Props = {
label: string,
children: ReactElement,
className?: string,
style?: CSSProperties,
onClick?: (e: MouseEvent<HTMLButtonElement>) =>void,
}

const FunctionComponent: FC<Props> = ({ label, children, onClick, ...resetProps }: Props) => {
return (
<div {...resetProps}>
<span>{label}:</span>
<button type="button" onClick={onClick}>
{children}
</button>
</div>
)
}

export default FunctionComponent

默认属性

如果,需要默认属性,可以通过默认参数值来处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { FC, ReactElement, MouseEvent  } from'react'

type Props = {
label?: string,
children: ReactElement,
}

const FunctionComponent: FC<Props> = ({ label = 'Hello', children }: Props) => {
return (
<div>
<span>
{label}:
</span>
<button type="button">
{children}
</button>
</div>
)
}

export default FunctionComponent
  • Class Components

相对于FC,多了 state,采用如下形式来定义Class Component

这一部分的写法,与TypeScript 2.8下的终极React组件模式相同,觉得结构很清晰,复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React, { Component } from'react';

type Props = {
label: string
}

const initialState = {
count: 0
}

type State = Readonly<typeof initialState>

class ClassCounter extends Component<Props, State> {
readonly state: State = initialState

private handleIncrement = () =>this.setState(Increment)

render() {
const { handleIncrement } = this;
const { label } = this.props;
const { count } = this.state;

return (
<div>
<span>
{label}: {count}
</span>
<button type="button" onClick={handleIncrement}>
{`Increment`}
</button>
</div>
)
}
}

export const Increment = (preState: State) => ({ count: preState.count + 1 })

export default ClassCounter

默认属性

处理 Class Component 的默认属性,主要有两种方法:

  • 一是定义高阶组件,例如TypeScript 2.8下的终极React组件模式中,利用 withDefaultProps 来定义默认属性,涉及组件的属性的类型转换;
  • 二是利用 static props 以及 componentWillReceiveProps,处理默认属性。

具体业务中,视情况而定,第一中可以查看相关文章,这里介绍第二种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

import React, { Component } from'react';

type Props = {
label: string,
initialCount: number
}

type State = {
count: number;
}

class ClassCounter extends Component<Props, State> {
static defaultProps = {
initialCount: 1,
}
// 依据 defaultProps 对 state 进行处理
readonly state: State = {
count: this.props.initialCount,
}
private handleIncrement = () =>this.setState(Increment)
// 响应 defaultProps 的变化
componentWillReceiveProps({ initialCount }: Props) {
if (initialCount != null && initialCount !== this.props.initialCount) {
this.setState({ count: initialCount })
}
}

render() {
const { handleIncrement } = this;
const { label } = this.props;
const { count } = this.state;

return (
<div>
<span>
{label}: {count}
</span>
<button type="button" onClick={handleIncrement}>
{`Increment`}
</button>
</div>
)
}
}

export const Increment = (preState: State) => ({ count: preState.count + 1 })

export default ClassCounter
  • 通用组件 Generic Components
  • 复用共有的逻辑创建组件
  • 常用于通用列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { Component, ReactElement } from'react'

interface GenericListProps<T> {
items: T[],
itemRenderer: (item: T, i: number) => ReactElement,
}

class GenericList<T> extends Component<GenericListProps<T>, {}> {
render() {
const { items, itemRenderer } = this.props

return <div>{items.map(itemRenderer)}</div>
}
}

export default GenericList
  • Render Callback & Render Props

  • Render Callback,也被称为函数子组件,就是将 children 替换为 () => children;

  • Render Props,就是将 () => component 作为 Props 传递下去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

import React, { Component, ReactElement } from'react';

type Props = {
PropRender?: () => ReactElement,
children?: () => ReactElement
}

class PropRender extends Component<Props, {}> {

render() {
const { props: { children, PropRender } }: { props: Props } = this;

return (
<div>
{ PropRender && PropRender() }
{ children && children() }
</div>
)
}
}

export default PropRender

// 应用
<PropsRender
PropRender={() => (<p>Prop Render</p>)}
>
{ () => (<p>Child Render</p>) }
</PropsRender>
  • HOC(Higher-Order Components)

简单理解为,接受React组件作为输入,输出一个新的React组件的组件的工厂函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

import * as React from'react'

interface InjectedProps {
label: string
}

exportconst withState = <BaseProps extends InjectedProps>(
BaseComponent: React.ComponentType<BaseProps>
) => {
type HocProps = BaseProps & InjectedProps & {
initialCount?: number
}
type HocState = {
readonly count: number
}

returnclass Hoc extends React.Component<HocProps, HocState> {
// 方便 debugging in React-Dev-Tools
static displayName = `withState(${BaseComponent.name})`;
// 关联原始的 wrapped component
static readonly WrappedComponent = BaseComponent;

readonly state: HocState = {
count: Number(this.props.initialCount) || 0,
}

handleIncrement = () => {
this.setState({ count: this.state.count + 1 })
}

render() {
const { ...restProps } = this.props asany
const { count } = this.state

return (
<>
{count}
<BaseComponent
onClick={this.handleIncrement}
{...restProps}
/>
</>
)
}
}
}

Redux - 使用以及 Redux Thunk 使用

以如下形式来介绍Redux,主要是in-ts的使用:

  • (prestate, action) => state;

  • 使用Redux Thunk 来出来异步操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// store.js

type DataType = {
counter: number
}

const DataState: DataType = {
counter: 0
}

type RootState = {
Data: DataType
}

export default RootState
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// action.js
import { Action, AnyAction } from'redux'
import { ThunkAction, ThunkDispatch } from'redux-thunk'
import RootState from'../store/index'

type IncrementPayload = {
value: number
}

interface IncrementAction extends Action {
type: 'INCREMENT',
payload: IncrementPayload
}

exportconst Increment = ({ value }: IncrementPayload): IncrementAction => {
const payload = { value }
return {
type: 'INCREMENT',
payload
}
}

export type DecrementPayload = {
value: number;
};

export interface DecrementAction extends Action {
type: 'DECREMENT';
payload: DecrementPayload;
}

export type RootAction = IncrementAction & DecrementAction;

exportconst asyncIncrement = (
payload: IncrementPayload
): ThunkAction<Promise<void>, RootState, void, AnyAction> => {
returnasync (dispatch: ThunkDispatch<RootState, void, AnyAction>): Promise<void> => {
returnnewPromise<void>((resolve) => {

console.log('Login in progress')
setTimeout(() => {
dispatch(Increment(payload))
setTimeout(() => {
resolve()
}, 1000)
}, 3000)
})
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

// reducer.js
import { DataState, DataType } from'../store/Data'
import { RootAction } from'../actions/'

exportdefaultfunction (state: DataType = DataState, { type, payload }: RootAction): DataType{
switch(type) {
case'INCREMENT':
return {
...state,
counter: state.counter + payload.value,
};
default:
return state;
}
}

// Hearder.js
import React, { Component, ReactNode } from'react'
import RootState from'../store/index'
import { Dispatch, AnyAction } from'redux'
import { ThunkDispatch } from'redux-thunk'
import { connect } from'react-redux'
import { Increment, asyncIncrement } from'../actions/'

const initialState = {
    name: 'string'
}

type StateToPropsType = Readonly<{
    counter: number
}>
type DispatchToPropsType = Readonly<{
    handleAdd: () =>void,
    handleDec: () =>void
}>

type StateType = Readonly<typeof initialState>
type PropsType = {
    children?: ReactNode
}
type ComponentProps = StateToPropsType & DispatchToPropsType & PropsType

class Header extends Component<ComponentProps, StateType> {
    readonly state: StateType = initialState;

    render() {
const { props: { handleAdd, handleDec, counter }, state: { name } } = this

return (
            <div>
                计数:{counter}
                <button onClick={handleAdd}>+</button>
                <button onClick={handleDec}>-</button>
            </div>
        )
    }

    private handleClick = () => this.setState(sayHello);
}

const sayHello = (prevState: StateType) => ({
    name: prevState.name + 'Hello world',
})

const mapStateToProps = (state: RootState, props: PropsType): StateToPropsType => {
    return {
        counter: state.Data.counter
    }
}

const mapDispatchToProps = (dispatch: ThunkDispatch<RootState, void, AnyAction>): DispatchToPropsType => {
    return {
        handleAdd: () => {
            dispatch(Increment({ value: 2 }))
        },
        handleDec: async () => {
            dispatch(asyncIncrement({ value: 10 }))
        }
    }
}

export default connect<StateToPropsType, DispatchToPropsType, PropsType, RootState>(mapStateToProps, mapDispatchToProps)(Header)