在软件开发中,反模式被认为是糟糕的编程实践的特定模式。在React组件开发中,我们可能会不小心地陷入反模式的陷阱,编写一些违背编程框架和思想的代码。
了解一些常见的反模式,有助于我们避开这些错误,增进对React这一个框架工作原理的理解,正确的掌握React开发的方法。
而了解一些常用的组件设计原则则有助于增强我们的代码设计水平,编写更加漂亮和高效的代码。
本文结合个人项目实践,对常见的几种组件反模式进行总结,并给出简洁的代码说明,主要包括以下三种:
- 用 prop 初始化组件的状态
- 直接修改组件内部状态
- 使用数组index作为key
- 在render中使用状态缓存
同时对比较重要的组件设计原则进行归纳和总结说明,主要有:
- 单功能原则
- 单一数据源原则
- 容器组件和表现组件原则
反模式
反模式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的控制。
我们需要尽力保持的就是:
- 组件单方面接收props的变量,但不改变它;
- 组件内部维护state变量,外部组件不改变它。
这其实是容器组件和表现组件的差别。下面稍微展开讲解。
容器组件和表现组件
容器组件负责保存数据和组织数据,表现组件只负责接受容器组件的数据并进行渲染。
容器组件内部可能嵌套着多个表现组件和容器组件,从顶层组件往下到表现组件构成一颗组件树。
我们通常会在容器组件中获取数据并且将数据传输给子组件,并且会使用一些生命周期函数,以类的方式来定义。
而表现组件只负责接收数据并渲染UI,没有特殊要求一般没有生命周期函数,使用函数进行编写即可。
参考文章
- 【1】 知乎:如何理解React.js中组件的反模式
- 【2】《React 设计模式与最佳实践》第11章需要避免的反模式
- 【3】React 组件设计技巧
- 【4】Github repo: react-pattern
- 【5】react反模式——将数组的index作为key