react初学,一个简单的TodoList

初次学习 react, 通读了中文官方文档的快速开始部分,收获挺多。就用一个简单的 todolist 来捋一捋学到的东西以及踩过的坑。重点部分在组件(受控/非受控),state,props 的设计以及 render 方法等。因为是简单的示例,所以样式上就比较随意。
由于是初学,我们这里就是用 script 标签引入 react.development.js,react-dom.development.js 以及 babel.min.js 三个文件。

先来看一个实现后的简单效果图:

效果图

输入框输入新的内容后,点击 + 按钮,将新的内容添加到下面的待办事项列表中;同时,每个待办事项都有一个删除按钮,点击即可删除。

组件划分

组件划分我们采用自顶向下的方式。

单一功能原则,在理想状况下,一个组件应该只做一件事情。如果这个组件功能不断丰富,它应该被分成更小的组件。

我们将整个 todolist 看做一个 App,为最上层的组件(App),那其应该包含两个子组件:新增待办事项组件(NewItemInput)和待办事项表格(TodoListTable)。为了更好的独立维护待办事项(这里主要是删除),TodoListTable 组件又由待办事项列表组件(TodoListItem)。

所以大致的结构如下:

1
2
3
4
5
6
7
8
9
<App>
<NewItemInput></NewItemInput>
<TodoListTable>
<TodoListItem></TodoListItem>
<TodoListItem></TodoListItem>
<TodoListItem></TodoListItem>
...
<TodoListTable/>
</App>

state 划分

React 有两种数据模型,props 和 state。

props 通常认为是组件提供给父组件的 api,即父组件可以通过子组件定义的 props 将数据传递给子组件。

state 通常被看做是组件用来维护自身状态的数据模型。

哪些数据是 state

我们按照以下几点考虑某一数据是否被看做是 state。

  • 它是通过 props 从父级传来的吗?如果是,他可能不是 state。
  • 它随着时间推移不变吗?如果是,它可能不是 state。
  • 你能够根据组件中任何其他的 state 或 props 把它计算出来吗?如果是,它不是 state。

我们的数据主要有两个: 待办事项的列表 todoList 以及输入框数据 inputText。无论是 todoList 还是 inputText,都是组件(App)自身维护的数据,不来自任何其他的外部组件,且二者都是随着时间变化而变化的,所以我们认为两个都是 state。

state 该放在哪里一层维护

从简单的数据来源和用途来看,todoList 似乎应该放在 TodoListTable 维护,而 inputText 似乎应该放在 NewItemInput 维护。然而并非这样,组件放在哪一层维护,我们考虑以下几点:

  • 确定每一个需要这个 state 来渲染的组件。
  • 找到一个公共所有者组件(一个在层级上高于所有其他需要这个 state 的组件的组件)
  • 这个公共所有者组件或另一个层级更高的组件应该拥有这个 state。
  • 如果你没有找到可以拥有这个 state 的组件,创建一个仅用来保存状态的组件并把它加入比这个公共所有者组件层级更高的地方。

显然,这里对于 todoList 而言,NewItemInput 组件需要改变它的值;而对于 inputText 而言 TodoListTable 需要渲染它的值。所以,我们将两个 state 都放在两个子组件公有的父组件 App 中维护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
App组件
//class 方法构建组件
//组件还可以通过纯 function 的方式构建
//其本质就是一个 function
class App extends React.Component{
constructor(props){
super(props);
this.state = {
todoList: [], //待办事项列表
inputText: "", //新增待办事项输入框
}
}

render(){//组件需要返回渲染函数
return (
<div>//组件有且只有一个元素包裹
<NewItemInput /> //新增输入框组件
<TodoListTable />//待办事项列表组件
</div>
);
}
}

完善各个组件

考虑 TodoListTable 和 NewItemInput 组件

TodoListTable 至少有一个名叫 todoList 的 props 来接收父组件传递的列表数据,以此来渲染出列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TodoListTable组件
class TodoListTable extends React.Component{
constructor(props){
super(props);
}

render(){
//循环生成列表
//添加唯一的 key 在循环生成的列表中为最佳实践,可以给 react 提供优化参考
//value 为 TodoListItem 提供的 props,用来渲染文本
let listItems = this.props.todoList.map((item, index)=>
<TodoListItem key={index} index={index} value={item} />);
return (
<ul>
{listItems}
</ul>
);
}
}

NewItemInput 包含了一个文本输入框 input 以及一个添加按钮 button,其父组件为 App。

这里采用的是 input 的受控组件模式(事实上可以使用非受控组件模式,也更简单点),即 input 组件的 value 值以及显示在 ui 中的文本/数据来自于外部组件,因此,你的 ui 中的数据(value)就可以和外部的数据源(state) 是实时同步的。参考这里受控组件与非受控组件

通常 React 是单向数据流,即数据是从父组件单向传递给子组件。

在这里,App 中 inputText 值的改变以及 input 的 value 值的改变相互影响,所以新增的待办事项信息是双向数据的。具体数据流如下:

  • input 中的数据发生了变化,触发 onChange 事件 (handleTextInputChange)
  • onChange 事件中调用了 App 通过 props 传递给 NewItemInput 的 onTextInput 方法,并将当前输入框的value(e.target.value) 作为参数传递
  • App 组件在 onTextInput 修改了 inputText 这个 state 的值
  • App 组件又将 inputText 的值通过 inputText 这个 props 传递给 NewItemInput 组件中 input 的 value 值

添加时的数据流如下:

  • 点击添加 + 按钮时,触发 button 的 onClick 事件(handleClick)
  • onClick 事件中调用了 App 通过 props 传递给 NewItemInput 的 onAdd 方法
  • App 组件在 onAdd 方法中修改了 todoList 这个 state 的值,为其增加一项

下面是 NewItemInput 的完整组件:

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
NewItemInput组件
class NewItemInput extends React.Component{
constructor(props){
super(props);

//事件绑定需要通过 bind 指定 this caller 的值为 this
this.handleClick = this.handleClick.bind(this);
this.handleTextInputChange = this.handleTextInputChange.bind(this);

}

//input 输入框变化事件
handleTextInputChange(e) {
this.props.onTextInput(e.target.value); //调用 App 组件传递的方法
}

//button 按钮点击事件
handleClick(e){
e.stopPropagation();
this.props.onAdd();//调用 App 组件传递的方法
}

render(){
return (
<div>//提供 inputText 作为 props
<input type="text" value= {this.props.inputText} onChange={this.handleTextInputChange} />
<button onClick={this.handleClick}>+</button>
</div>
);
}
}

我们完善下 App 组件关于 NewItemInput 的部分:

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

class App extends React.Component{
constructor(props){
super(props);
this.state = {
todoList: [],
inputText: "",
}
this.addNewItem = this.addNewItem.bind(this);
this.textInput = this.textInput.bind(this);
}

addNewItem(){
this.setState({
//将当前的 inputText 值添加到 todoList 中
todoList: [...this.state.todoList, this.state.inputText],
inputText: ''//添加成功后,清除输入框
});
}

textInputChange(val){
this.setState({
inputText:val //每次input输入框的值发生变化时,修改 state 的值
});
}

render(){
return (
<div>
//为 NewItemInput 三个 props 绑定值或者方法
<NewItemInput onAdd={this.addNewItem} inputText={this.state.inputText} onTextInput={this.textInputChange}/>
//todoList 通过 props 传递给 TodoListTable
<TodoListTable todoList={this.state.todoList} />
</div>
);
}
}
完善 TodoListItem 组件以及删除功能

TodoListItem 组件即每个列表项的组件。

对每个列表项而言,除了需要渲染其值,还需要提供一个删除接口。因此,我们除了提供名叫 value 的 props 还需要提供名叫 onDeleteOne 的 props,而且需要通过 index 这个 props 给该方法传递一个 index,方便查找需要删除 todoList 中的哪一个。这里的数据流和添加基本一样,不做详细解释。

下面是 TodoListItem 的完成代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
TodoListItem组件
class TodoListItem extends React.Component{
constructor(props){
super(props);
this.deleteOneItem = this.deleteOneItem.bind(this);
}

deleteOneItem(e){
e.stopPropagation();
this.props.onDeleteOne(this.props.index);
}

render(){
return (
<li>
<span>{this.props.value}</span>
<span onClick={this.deleteOneItem}>删除</span>
</li>
);
}
}

由于 TodoListItem 的父组件是 TodoListTable,所以通过 props 给 TodoListItem 传值和方法的是 TodoListTable 组件。

同时,todoList 这个 state 的维护是在 App 中进行的,所以,想要在点击删除按钮时修改 todoList 值,就必须在 TodoListTable 中为 App 提供一个 props使,得在调用 onDeleteOne 的时候能够真正调用到 App 中的方法,从而修改 todoList 的值,我们将这个 props 命名为 onDeleteItem。

下面是完整的 TodoListTable 和 App 的代码:

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
63
64
65
66
67
68
69
70
71
72
73
TodoListTable组件
class TodoListTable extends React.Component{
constructor(props){
super(props);
this.deleteOne = this.deleteOne.bind(this);
}

deleteOne(index){
//调用 App 组件通过 props 传递的方法
this.props.onDeleteItem(index);
}

render(){
//循环生成列表
//添加唯一的 key 在循环生成的列表中为最佳实践,可以给 react 提供优化参考
//value 为 TodoListItem 提供的 props,用来渲染文本
let listItems = this.props.todoList.map((item, index)=>
//onDeleteOne 通过 props 将 deleteOne 方法传递给 TodoListItem
<TodoListItem key={index} index={index} value={item} onDeleteOne={this.deleteOne} />);
return (
<ul>
{listItems}
</ul>
);
}
}


App组件
class App extends React.Component{
constructor(props){
super(props);
this.state = {
todoList: [],
inputText: "",
}
this.addNewItem = this.addNewItem.bind(this);
this.deleteItem = this.deleteItem.bind(this);
this.textInput = this.textInput.bind(this);
}

addNewItem(){
this.setState({
//将当前的 inputText 值添加到 todoList 中
todoList: [...this.state.todoList, this.state.inputText],
inputText: ''//添加成功后,清除输入框
});
}

deleteItem(index){
this.state.todoList.splice(index, 1);
this.setState({
todoList: this.state.todoList
});
}

textInputChange(val){
this.setState({
inputText:val //每次input输入框的值发生变化时,修改 state 的值
});
}

render(){
return (
<div>
//为 NewItemInput 三个 props 绑定值或者方法
<NewItemInput onAdd={this.addNewItem} inputText={this.state.inputText} onTextInput={this.textInputChange}/>
//todoList 通过 props 传递给 TodoListTable
<TodoListTable todoList={this.state.todoList} onDeleteItem={this.deleteItem} />
</div>
);
}
}

挂载

最后,我们将 App 挂载到 html 的容器中:

1
2
3
4
5
6
7
8

<div id="app"></div>
...

ReactDOM.render(
<App />,
document.getElementById('app')
);

省略号部分即为前面介绍的所有组件。

踩的坑

限于篇幅原因,简单记录下踩的坑,后期可能会对个别比较重要的坑做独立探讨。

  • 组件的 render 方法必须有且只有一个根元素
  • 组件中需要显式的使用 bind 给方法指定上下文为 this, 否则渲染后找不到调用它的对象
  • setState 在修改数组的时候不能直接用 push/pop 修改数组本身,而是需要使用[… val]/concat[val]/splice(index,1) 等方法来返回一个新的数组,并将该数组作为 setState 的新值
  • 关于 setState 还有一些特性后续再讨论,比如 setState 是异步的等
  • 本文书写的是时候,只是对 React 初学的一个总结,还未学习到一些深入的知识,所以在实现上可能饶了一点弯路。
Loading comments box needs to over the wall