组件思维 - 创建一个todo app
本节我们将学习:
- 如何将一个app拆分成一个一个的组件
- 概念介绍:presentation/dumb组件和容器组件
组件思维简单来说就是将app拆分成一个一个的组件。以搭建一个简单待办事项应用程序为例,它可以拆分很多组件,我们一起来完成它。
todos列表
我们从待办事项列表开始设计,在考虑展示数据前,先设计底层数据结构。
首先待办事项有描述字段,然后考虑如何完成一个待办事项。待办事项应该包含一个事项列表,因为我们希望在某个时间点执行清单上的一些事项。在事项完成后,是把事项删除还是标记完成呢?我们选择后者,为了让自己对完成它感到满意,同事还可以随时查看以前所做的事情。我们的数据结构的信息。它很可能看起来像这样:
[
{
title: 'clean',
done: false
}, {
title: 'do dishes',
done: false
}
]
草拟的数据结构设计完成,尝试在 React 中渲染它。
渲染列表
先来试一试:
{todos.map(todo => (
<div>
<input type="checkbox" checked={todo.done} /> {todo.title}
</div>
)}
呈现了待办事项列表,但不能修改待办事项的值,我们需要构建一个真正的 React 组件类,添加事项更改功能。
import React, { Component } from 'react';
import styled from 'styled-components';
const todos = [
{
title: 'clean',
done: false,
},
{
title: 'do the dishes',
done: true,
}
];
const Todos = styled.div`
padding: 30px;
`;
const Todo = styled.div`
box-shadow: 0 0 5px gray;
padding: 30px;
margin-bottom: 10px;
`;
class App extends Component {
render() {
return (
<Todos>
<h2>Todos</h2>
{todos.map(todo => (
<Todo>
<input type="checkbox" checked={todo.done} /> {todo.title}
</Todo>
))}
</Todos>
);
}
}
export default App;
需要安装 styled-components 支持 npm install --save styled-components
上面我们已经创建了一个完整的组件,但还没有添加对更改待办事项的支持。让我们接下来这样做。我们需要考虑:
- 为 复选框 添加监听
onChange事件 - 修改待办事项,调用事件绑定事件的方法
使用箭头函数 绑定 onChange事件 和监听函数:
<input type="checkbox" checked={todo.done} onChange={() => this.handleChange(todo)} />
然后找出一种方法来更改列表中的待办事项。我们可以直接改变 todos 列表,但在 React更好的方式是在组件中创建一个 todos 状态,就像这样:
state = {
todos
}
最终代码:
import React, { Component } from 'react';
import styled from 'styled-components';
const todos = [
{
title: 'clean',
done: false,
id: 1,
},
{
title: 'do the dishes',
done: true,
id: 2,
}
];
const Todos = styled.div`
padding: 30px;
`;
const Todo = styled.div`
box-shadow: 0 0 5px gray;
padding: 30px;
margin-bottom: 10px;
`;
class App extends Component {
state = {
todos,
};
handleChecked = (todo) => {
const newTodos = this.state.todos.map(t => {
if (t.id === todo.id) {
return { ...t, done: !t.done };
}
return t;
});
this.setState({
todos: newTodos,
});
}
render() {
return (
<Todos>
<h2>Todos</h2>
{this.state.todos.map(todo => (
<Todo key={todo.id}>
<input type="checkbox" onChange={() => this.handleChecked(todo)} checked={todo.done} />
{todo.title}
</Todo>
))}
</Todos>
);
}
}
export default App;
handleChecked() 是onChange绑定的方法,点击修改复选框:
handleChecked = (todo) => {
const newTodos = this.state.todos.map(t => {
if (t.id === todo.id) {
return { ...t, done: !t.done };
}
return t;
});
this.setState({
todos: newTodos,
});
}
通过遍历找到被点击的待办事项,使用对象展开操作符修改:
return { ...t, done: !t.done }
注意todo 现在由三个属性组成:
- title
- done
- id
id 属性用来标识修改哪个 todo.
创建 todos 组件
现在我们完成了App的基本功能,把所有功能逻辑放到一起看起来很混乱,现在我们通过创建 Todos 组件来重构代码,代码将拆分成下面的文件:
- App.js - App入口文件
- Todos.js - 列表逻辑文件
将列表逻辑文件 挪到 Todos.js 中
// Todos.js
import React, { Component } from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
const TodosContainer = styled.div`
padding: 30px;
`;
const Todo = styled.div`
box-shadow: 0 0 5px gray;
padding: 30px;
margin-bottom: 10px;
`;
class Todos extends Component {
static propTypes = {
todos: PropTypes.array.isRequired,
}
state = {
todos: this.props.todos,
};
handleChecked = (todo) => {
const newTodos = this.state.todos.map(t => {
if (t.id === todo.id) {
return { ...t, done: !t.done };
}
return t;
});
this.setState({
todos: newTodos,
});
}
render() {
return (
<TodosContainer>
<h2>Todos</h2>
{this.state.todos.map(todo => (
<Todo key={todo.id}>
<input type="checkbox" onChange={() => this.handleChecked(todo)} checked={todo.done} /> {todo.title}
</Todo>
))}
</TodosContainer>
);
}
}
export default Todos;
自从React 15.5.0后,PropTypes需要用户自己引入 npm install --save prop-types
App入口文件如下:
import React, { Component } from 'react';
import Todos from './Todos';
const todos = [
{
title: 'clean',
done: false,
id: 1,
},
{
title: 'do the dishes',
done: true,
id: 2,
}
];
class App extends Component {
render() {
return (
<Todos todos={todos} />
);
}
}
export default App;
拆分 todos 组件
我们看到Todos.js组件包含了不少内容. 我们可以继续按照责任原则拆分它. 虽然它运行起来没问题,但通常我们会把它拆分成更小的组件,这样更容易管理和扩展。另外,更小的组件可以让组件更易于被调用。如何拆分Todos.js 组件呢?
- Todos组件, 渲染Todo组件列表,负责处理所有事件
- Todo组件,负责渲染Todo和向上发送任何更改操作
重构代码,添加Todo.js :
// Todo.js
import React from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
const TodoContainer = styled.div`
box-shadow: 0 0 5px gray;
padding: 30px;
margin-bottom: 10px;
`;
const Todo = ({ todo, handleChecked }) => (
<TodoContainer key={todo.id}>
<input type="checkbox" onChange={() => handleChecked(todo)} checked={todo.done} />
{todo.title}
</TodoContainer>
);
Todo.propTypes = {
todo: PropTypes.shape({
title: PropTypes.string,
done: PropTypes.bool,
id: PropTypes.number,
}),
handleChecked: PropTypes.func,
};
export default Todo;
把Todo放到单独的组件中。该组件不继承React,是一个简单的函数组件,通常称为演示或哑组件,它对上下文一无所知,只依赖于输入。 todo的结果依赖于 传入的方法· handleChecked() 和 props。
Todos.js 修改为:
// Todos.js
import React, { Component } from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import Todo from './Todo';
const TodosContainer = styled.div`
padding: 30px;
`;
class Todos extends Component {
static propTypes = {
todos: PropTypes.array.isRequired,
}
state = {
todos: this.props.todos,
};
handleChecked = (todo) => {
const newTodos = this.state.todos.map(t => {
if (t.id === todo.id) {
return { ...t, done: !t.done };
}
return t;
});
this.setState({
todos: newTodos,
});
}
render() {
return (
<TodosContainer>
<h2>Todos</h2>
{this.state.todos.map(todo => (
<Todo todo={todo} key={todo.id} handleChecked={this.handleChecked} />
))}
</TodosContainer>
);
}
}
export default Todos;
在Todos.js 引入Todo组件:
import Todo from './Todo';
调用Todo组件
<Todo todo={todo} key={todo.id} handleChecked={this.handleChecked} />
上述代码中,通过传入 数据 todo 和方法 handleChecked() 给 Todo 组件,在组件中完成渲染,这种组件称为展示组件。
还有一种组件叫容器组件,容器组件是数据和逻辑所在的组件,它“包含”了应用程序的内容。
在我们的App中 Todo组件是展示组件,Todos组件是容器组件。践行组件思维构建的App应该包含大部分展示组件和少部分容器组件。
总结
创建了一个待办事项App。
把App拆分成若干个专注某一项功能的小组件,小的组件便于后期维护和调用,也更简单。
介绍了两个术语,展示/哑组件和容器组件。解释了它们的概念,它们的区别处决于传入的内容。