React 学习 | 常见的反模式和组件设计原则总结

在软件开发中,反模式被认为是糟糕的编程实践的特定模式。在React组件开发中,我们可能会不小心地陷入反模式的陷阱,编写一些违背编程框架和思想的代码。

了解一些常见的反模式,有助于我们避开这些错误,增进对React这一个框架工作原理的理解,正确的掌握React开发的方法。
而了解一些常用的组件设计原则则有助于增强我们的代码设计水平,编写更加漂亮和高效的代码。

本文结合个人项目实践,对常见的几种组件反模式进行总结,并给出简洁的代码说明,主要包括以下三种:

  1. 用 prop 初始化组件的状态
  2. 直接修改组件内部状态
  3. 使用数组index作为key
  4. 在render中使用状态缓存

同时对比较重要的组件设计原则进行归纳和总结说明,主要有:

  1. 单功能原则
  2. 单一数据源原则
  3. 容器组件和表现组件原则

反模式


反模式1:使用 prop 初始化状态

我们不推荐使用 prop 来初始化组件的状态,比如下面就是一个反例。

class Demo extends React.Component {
	constructor(props) {
		super(props)
		this.state = {num: props.num}
	}
}

这样做的后果会是怎样呢?看下面这个例子。

class Demo extends React.Component {
	constructor(props) {
		super(props)
		this.state = {num: props.num}
		this.handleClick = this.handleClick.bind(this)
	}
	handleClick() {
		this.setState({num: this.state.num + 1})
	}
	render(){
		return (
		<div>
			<button onClick={this.handleClick}> {this.state.num} </button>
		</div> 
		)
	}
}

上面的组件功能就是每一次点击按钮,组件state中的num就会递增,但是由上层传下来的props.num并不会存在变化。
这样的坏处是违背了单一数据源的原则,组件初始状态来自props,而后续状态由自身state决定,而不是props(父组件无法修改子组件的num)。

这种编写方式容易引起误会和调试的困难,我们应该尽量把数据按照容器和表现组件的原则进行分离。不过我们还是在某些特定的情况下使用这种模式,只不过我们最好阐释以下这种做法的用意,比如你只是想用prop初始化子组件,后续不对子组件状态进行控制,那么你应该为属性定义一个具有清晰含义的名称。
比如 initialNum。
就像这样:

class Demo extends React.Component {
	constructor(props) {
		super(props)
		this.state = {num: props.initialNum}
		this.handleClick = this.handleClick.bind(this)
	}
	handleClick() {
		this.setState({num: this.state.num + 1})
	}
	render(){
		return (
		<div>
			<button onClick={this.handleClick}> {this.state.num} </button>
		</div> 
		)
	}
}
// 然后这样使用:
<Demo initialNum={2} />

而且需要注意一点就是不要修改props的值,这违背了React单一数据流动的原则。

反模式2 : 直接修改状态

React 中 提供了非常直观的做法来修改组件内部的状态——通过setState的方法告诉组件如何修改状态。我们不允许使用直接修改this.state里变量的方法,因为这样会导致:

  • 状态改变不会出发组件重新渲染;
  • 无论合适调用setState,之前修改的状态会渲染到页面上。
// bad
handleClick() {
	this.state.num++;
}

// good
handleClick(){
	this.setState({num: this.state.num++});
}

反模式3:将数组索引作为Key

这里简单的讲以下Key属性。Key属性是唯一的标识了DOM中的某一个元素,React会使用Key来判断元素是否为新的,以及组件属性和状态改变时是否要更新元素。

比较常见的应用场景就是一个列表组件,内部需要渲染多个子组件,我们通常需要Key来标识。下面给出一个简单的demo。

class Demo extends React.Component{
	constructor(props){
		super(props)
		this.state = {
			items: ["a","b","c"]
		}
		this.handleAddItem. = this.handleAddItem.bind(this)
	}
	handleAddItem(){
		let newItem = this.state.items.slice()
		newItem.unshift('d') // 在数组顶部插入元素
		this.setState({items: newItem}).
	}
	render(){
		<div>
			<ul>
				{  // item是数组内容,index是数组索引
				this.state.items.map((item, index) => {
					<li key={index}>{item}<input type="text" /></li>	
				}
				}
			</ul>
		</div>
	}
}

如果使用了索引作为Key,对数组头部进行元素的插入,对于一个已经挂载到DOM树上的DOM节点来说,其Key值不变,React不会对该DOM进行更新,而只会更新内部的属性值。
一个简单的例子如下:

* a   [    输入值               ]  ---> 输入框   key = 0
* b   [                         ]               key = 1
* c   [                         ]               key = 2

当我们在头部插入 d 时,此时的变化会是:

* d   [    输入值               ]  ---> 输入框   key = 0
* a   [                         ]               key = 1
* b   [                         ]               key = 2
* c   [                         ]               key = 3

此时输入值不会随着 字母 a 进行移动,因为此时的Key没有发生变化,React仅仅会更新属性值 b,a,c,d的位置。

正确的做法是,为数组的每一个元素指定一个唯一的标识符,比如 id,以下是一种可取的做法。

this.state = {
	maxID: 4,  // 每次添加一个value时,将maxID赋给该value对应的id,然后maxID++.
	items: [{value:a, id:1} {value:"b", id:2}, {value: "c", id: 3]
}

render(){
	<div>
		<ul>
			{  // item是数组内容
			this.state.items.map((item) => {
				<li key={item.id}>{item.value}<input type="text" /></li>	
			}
			}
		</ul>
	</div>
}

反模式4 : 在render中使用状态缓存

在 render 函数里头,我们可能会使用到一些状态变量,但是需要注意是,在render函数里头应该保留尽量少的状态变量,不要声明一些变量,然后在返回HTML里头使用。

以下是一个不建议的用法。

// bad
render () {
	let name = this.props.name
	return <div>{name}</div>
}

比较好的做法是,直接在html中使用状态变量。

// good
render () {
	return <div>{this.props.name}</div>
}

更加fancy的做法使用一个函数返回对应的状态变量。

// best
get fancyName(){
	return this.props.name;
}

render () {
	return <div>{this.fancyName}</div>
}

还有一点相关的就是不要在render使用复合条件。
一个错误示例就是:

// bad
render () {
  return <div>{if (this.state.happy && this.state.knowsIt) { return "Clapping hands" }</div>;
}

正确的做法依旧是将条件写在一个函数里头,根据条件判断返回对应的属性值。

// better
get isTotesHappy() {
  return this.state.happy && this.state.knowsIt;
},

render() {
  return <div>{(this.isTotesHappy) && "Clapping hands"}</div>;
}

设计原则

单功能原则

使用React的时候,组件或容器的代码在根本上必须只负责一块UI的功能。

我们不要定义一个具有许多功能的组件,这会导致组件的复杂性和难以维护,难以复用。

一个比较合格的组件尽量保证在200行代码内完成。

单一数据源原则

在分析一个组件内部数据的流动时,我们必须明确数据的来源和去向,以及相应的状态。

我们不允许一个数据的存在多个来源。就如上面反模式中使用 prop 初始化组件状态一样,我们不允许组件内部的状态来源于props然后又受组件内部setState的控制。

我们需要尽力保持的就是:

  1. 组件单方面接收props的变量,但不改变它;
  2. 组件内部维护state变量,外部组件不改变它。

这其实是容器组件和表现组件的差别。下面稍微展开讲解。

容器组件和表现组件

容器组件负责保存数据和组织数据,表现组件只负责接受容器组件的数据并进行渲染。

容器组件内部可能嵌套着多个表现组件和容器组件,从顶层组件往下到表现组件构成一颗组件树。

我们通常会在容器组件中获取数据并且将数据传输给子组件,并且会使用一些生命周期函数,以类的方式来定义。

而表现组件只负责接收数据并渲染UI,没有特殊要求一般没有生命周期函数,使用函数进行编写即可。

参考文章

  • 【1】 知乎:如何理解React.js中组件的反模式
  • 【2】《React 设计模式与最佳实践》第11章需要避免的反模式
  • 【3】React 组件设计技巧
  • 【4】Github repo: react-pattern
  • 【5】react反模式——将数组的index作为key

你可能感兴趣的:(React,前端)