案例需求
一个输入框, 下面显示任务列表, 最下面显示总共任务条数, 全选按钮, 以及清除已完成任务的按钮. 输入后回车添加任务到最上面, 鼠标移动到任务上面, 任务显示删除按钮, 确认后可以删除.
实现静态组件
首先, 最大的整个就是 App 组件, 但是我们不能就到这里了.
- 上面的输入框, 可以添加任务, 这就是一个单独的组件了, Header
- 下面的任务列表, 展示所有的任务, 是第二个组件, List
- 任务列表中的每一个任务, 都是一个组件, Item
- 最下面的部分是最后一个组件, Footer
下面, 就可以创建对应的四个组件了. (App 组件不算)
1 2 3 4 5 6 7 8 9 10
| import {Component} from "react";
export default class <组件名称> extends Component { render() { return ( <> </> ); } }
|
创建对应目录, 写入以上内容即可.
这里使用的是企业中常用的 index 写法, 而不是叫做 xxx. jsx 了
根据拆分的思想, 首先实现 Header, 其实就是一个输入框, 直接拿过来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import {Component} from "react";
import './index.css'
export default class Header extends Component { render() { return ( <div className="todo-header"> <input type="text" placeholder="请输入你的任务名称, 按下回车创建"/> </div> ); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| .todo-header input { width: 560px; height: 28px; font-size: 14px; border: 1px solid #ccc; border-radius: 4px; padding: 4px 7px; }
.todo-header input:focus { outline: none; border-color: rgba(82, 168, 236, 0.8); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); }
|
List 部分有些复杂, 我们先实现最下面的 Footer 组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import {Component} from "react"; import './index.css'
export default class Footer extends Component { render() { return ( <div className="todo-footer"> <label> <input type="checkbox"/> </label> <span> <span>已完成0</span> / 全部2 </span> <button className="btn btn-danger">清除已完成任务</button> </div> ); } }
|
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
| .todo-footer { height: 40px; line-height: 40px; padding-left: 6px; margin-top: 5px; }
.todo-footer label { display: inline-block; margin-right: 20px; cursor: pointer; }
.todo-footer label input { position: relative; top: -1px; vertical-align: middle; margin-right: 5px; }
.todo-footer button { float: right; margin-top: 5px; }
|
回到 List 组件, 里面是有很多个 Item 组件的, 所以可以让 List 组件布局如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import {Component} from "react";
import Item from "../Item/index.jsx";
import './index.css'
export default class List extends Component { render() { return ( <ul className="todo-main"> <Item/> </ul> ); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| .todo-main { margin-left: 0px; border: 1px solid #ddd; border-radius: 2px; padding: 0px; }
.todo-empty { height: 40px; line-height: 40px; border: 1px solid #ddd; border-radius: 2px; padding-left: 5px; margin-top: 10px; }
|
Item 就是列表的元素, 可以写出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import {Component} from "react"; import './index.css';
export default class Item extends Component { render() { return ( <li> <label> <input type="checkbox"/> <span>xxxxx</span> </label> <button className="btn btn-danger" style={{display: "none"}}>删除</button> </li> ); } }
|
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
| li { list-style: none; height: 36px; line-height: 36px; padding: 0 5px; border-bottom: 1px solid #ddd; }
li label { float: left; cursor: pointer; }
li label li input { vertical-align: middle; margin-right: 6px; position: relative; top: -1px; }
li button { float: right; display: none; margin-top: 3px; }
li:before { content: initial; }
li:last-child { border-bottom: none; }
|
至此, 回到 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
| import {Component} from "react";
import Header from "./components/Header"; import Footer from "./components/Footer"; import List from "./components/List";
import './App.css'
export default class App extends Component { render() { return ( <div className="todo-container"> <div className="todo-wrap"> <Header/> <List/> <Footer/> </div> </div> ) } }
|

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
| body { background: #fff; }
.btn { display: inline-block; padding: 4px 12px; margin-bottom: 0; font-size: 14px; line-height: 20px; text-align: center; vertical-align: middle; cursor: pointer; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); border-radius: 4px; }
.btn-danger { color: #fff; background-color: #da4f49; border: 1px solid #bd362f; }
.btn-danger:hover { color: #fff; background-color: #bd362f; }
.btn:focus { outline: none; }
.todo-container { width: 600px; margin: 0 auto; } .todo-container .todo-wrap { padding: 10px; border: 1px solid #ddd; border-radius: 5px; }
|
动态初始化列表
刚才的 List 中的内容都是写死的, 这显然不是我们想要的结果. 状态中的数据驱动了页面的展示, 这个状态储存在哪里呢?
考虑到, Header 也要输入状态, 目前的知识无法实现兄弟组件 (非父子关系) 之间的传参, 这就很不好了. 所以我们可以把状态存储在 App 中, 父子之间使用 props 进行传递, 反过来可以用函数的方式.
回到 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
| export default class App extends Component { state = { todos: [ {id: '001', name: '吃饭', done: false}, {id: '002', name: '睡觉', done: true}, {id: '003', name: '敲代码', done: false} ] }
render() { const {todos} = this.state; return ( <div className="todo-container"> <div className="todo-wrap"> <Header/> {/*List需要渲染 所以传递给List组件 另外这里不能简写了*/} <List todos={todos}/> <Footer/> </div> </div> ) } }
|
随后, 在 List 组件中接收参数 props.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export default class List extends Component { render() { const {todos} = this.props; return ( <ul className="todo-main"> {/*多少个item 取决于todos的长度*/} { todos.map((todo) => { return <Item key={todo.id}/> }) } </ul> ); } }
|

数量正确了, 但是我们需要的是里面的文字根据 todo 来进行渲染, 则可以继续传参.
1 2 3
| todos.map((todo) => { return <Item key={todo.id} todo={todo}/> })
|
这样, 在 Item 中, 则可以获取到 todo 了, 根据 todo 进行渲染即可.
这里需要注意, 最好不要用 checked 属性, 这会导致原来的按钮变得无法点击, 也就是只读.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| export default class Item extends Component { render() { const {todo} = this.props return ( <li> <label> <input type="checkbox" defaultChecked={todo.done}/> <span>{todo.name}</span> </label> <button className="btn btn-danger" style={{display: "none"}}>删除</button> </li> ); } }
|
至此, 任务渲染完毕, 一切顺利!
添加 TODO
回车添加 todo, 所以这是一个键盘事件的监听, 找到 Header, 添加一个键盘事件并绑定函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| export default class Header extends Component { handleKeyUp = (event) => { console.log(event.target.value) }
render() { return ( <div className="todo-header"> <input onKeyUp={this.handleKeyUp} type="text" placeholder="请输入你的任务名称, 按下回车创建"/> </div> ); } }
|

然而, 这不是我们想要的效果, 我们希望的是按下回车才会触发事件. 我们根据 event.keyCode 来判断按下的是什么按键. 只有 keyCode 为 13 才是按下回车. 另外也可以使用解构赋值进行代码简化:
1 2 3 4 5
| handleKeyUp = (event) => { if (event.keyCode !== 13) return; console.log(event.target.value) }
|
接下来, 就是想办法将用户的输入放入组件中. 我们的数据是 App 组件中的状态, 那么我们需要修改的就是 App 中的状态了, 溯源修改对应的数据. 父给子传东西可以直接使用 props, 反过来呢? 其实就是逆转思维了.
我们还是使用 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
|
addTodo = (todoObj) => { console.log("Add Todo", todoObj) const {todos} = this.state; const newTodos = [todoObj, ...todos]; this.setState({ todos: newTodos }) } render() { const {todos} = this.state; return ( <div className="todo-container"> <div className="todo-wrap"> {/*传入一个方法即可*/} <Header addTodo={this.addTodo}/> {/*List需要渲染 所以传递给List组件 另外这里不能简写了*/} <List todos={todos}/> <Footer/> </div> </div> ) }
|
回到 Header 组件中, 我们可以写出如下代码:
1 2 3 4 5 6 7 8 9
| handleKeyUp = (event) => { const {keyCode, target} = event; if (keyCode !== 13) return; const todoObj = {id: "", name: target.value, done: false}; this.props.addTodo(todoObj); }
|
这里的 ID 肯定不能是一样的, 那么我们可以怎么做呢? 这里推荐使用一个库: nanoid, 比较小的一个可以用来生成唯一 ID 的库. 在项目目录下运行命令即可安装: npm install nanoid
随后, 代码中引入这个函数即可随机创建一个 ID 出来:
1 2 3 4 5 6
| import {nanoid} from "nanoid";
...
const todoObj = {id: nanoid(), name: target.value, done: false};
|
至此, 已经可以添加内容了!
然而, 现在发现直接空格也是可以添加 todo 的, 这是我们不想要的, 所以再加一些判断即可.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| handleKeyUp = (event) => { const {keyCode, target} = event; if (keyCode !== 13) return; if (target.value.trim() === '') { alert("输入不能为空!") return; } const todoObj = {id: nanoid(), name: target.value, done: false}; this.props.addTodo(todoObj); }
|
鼠标移入效果
我希望, 鼠标悬浮上去后, 背景色能够自动修改, 并且后面出现一个删除按钮.
可以直接给单独的 Item 添加绑定事件, 监听移入和移出; 同时, 将状态保存到自身的 state 里面, 让样式, 以及后面的删除按钮根据 state 判断就好.
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
| export default class Item extends Component { state = { mouseIsIn: false }
handleMouse = (isMouseIn) => { return () => { this.setState({ mouseIsIn: isMouseIn }) } }
render() { const {todo} = this.props const {mouseIsIn} = this.state return ( <li style={{backgroundColor: mouseIsIn ? "#ddd" : "#fff"}} onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}> <label> <input type="checkbox" defaultChecked={todo.done}/> <span>{todo.name}</span> </label> <button className="btn btn-danger" style={{display: mouseIsIn ? "block" : "none"}}>删除</button> </li> ); } }
|
至此, 鼠标的移入效果, 悬浮效果已经实现!
勾选或取消勾选 TODO
勾选和取消勾选, 是一种变化, 也就是 onChange 监听了, 我们对前面的勾选框添加一个事件监听, 并调用回调函数.
另外, 考虑到我们勾选是需要知道 ID 的, 所以我们应当传入 ID, 这个函数同样需要进行柯里化.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| handleCheck = (id) => { return (event) => { console.log("id =", id) console.log("是否勾选", event.target.checked) } }
...
return ( <input type="checkbox" defaultChecked={todo.done} onChange={this.handleCheck(todo.id)}/> ... )
|
现在只是获取, 我还需要通知 App 来更改了. 这里需要注意, 层级关系为: App -> List -> Item, 存在一个多层嵌套的关系, 所以需要逐层进行传递.
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
| updateTodo = (id, done) => { console.log("调用了") const {todos} = this.state; const newTodos = todos.map((todoObj) => { if (todoObj.id === id) { return {...todoObj, done}; } else { return todoObj; } }) this.setState({ todos: newTodos }) }
render() { const {todos} = this.state; return ( <div className="todo-container"> <div className="todo-wrap"> <Header addTodo={this.addTodo}/> {/*传入函数 用来切换状态*/} <List todos={todos} updateTodo={this.updateTodo}/> <Footer/> </div> </div> ) }
|
回到 List 组件, 不需要调用, 继续传递即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export default class List extends Component { render() { const {todos, updateTodo} = this.props; return ( <ul className="todo-main"> {/*多少个item 取决于todos的长度*/} { todos.map((todo) => { return <Item key={todo.id} todo={todo} updateTodo={updateTodo}/> }) } </ul> ); } }
|
最后在 Item 组件中, 进行调用即可:
1 2 3 4 5 6 7 8 9
| handleCheck = (id) => { return (event) => { this.props.updateTodo(id, event.target.checked); } }
|
这样状态就可以正常更新了!
删除 TODO
给删除按钮添加一个删除事件, 我们肯定需要拿到 ID, 所以直接传入; 这里可以不使用高阶函数.
1 2 3 4 5 6 7 8 9 10 11
| handleDelete = (id) => { }
...
return ( <button onClick={() => this.handleDelete(todo.id)} className="btn btn-danger" style={{display: mouseIsIn ? "block" : "none"}}>删除 </button> )
|
在 App 中实现删除 todo 的方法, 然后逐层传递给 Item 进行调用即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| deleteTodo = (id) => { const {todos} = this.state; const newTodos = todos.filter((todoObj) => { return todoObj.id !== id; }) this.setState({ todos: newTodos }) }
|
正常调用即可得到正确的运行结果!
另外, 也可以加一个确认弹窗, 我们可以使用 confirm 来实现. 对于这个函数, 调用的时候需要明确是 window 身上的; 其次, 如果用户点击了是, 则会返回 true, 否则返回 false.
1 2 3 4 5
| handleDelete = (id) => { if (window.confirm("确认要删除该任务吗?")){ this.props.deleteTodo(id); } }
|
实现底部功能
首先, 实现已完成和全部多少的 文字部分. 全选暂时放在一边, 先实现简单一些的.
简单说, 只要把 todos 交给 Footer, 就可以实现统计了, 所以直接在 App 中传递 todos:
随后, 使用 reduce 函数以及数组本身的 length, 即可统计当前总共有多少, 以及完成了多少:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| render() { const {todos} = this.props; const total = todos.length; const doneTotal = todos.reduce((pre, todo) => { return pre + (todo.done ? 1 : 0); }, 0) return ( <div className="todo-footer"> <label> <input type="checkbox"/> </label> <span> <span>已完成 {doneTotal}</span> / 全部 {total} </span> <button className="btn btn-danger">清除已完成任务</button> </div> ); }
|
另外, 全选按钮也需要实现. 逻辑其实就是, 当选择的个数等于全部个数的时候勾选, 否则没有勾选.
1
| <input type="checkbox" checked={doneTotal === total}/>
|
另外, 我们也需要能够修改, 这里是不能设置 defaultChecked 的, 因为它的勾选与否是有功能作用的!
我们直接给它的勾选与否添加一个方法: onChange 即可.
对于之前的 Item 也需要设置为 checked, 不然无法正常监听更改!
对于后面的清除已完成, 只需要配合 filter 即可实现功能.
1 2 3 4 5 6 7 8 9 10 11 12 13
| clearDoneTasks = () => { const {todos} = this.state; const newTodos = todos.filter((todoObj) => { return !todoObj.done }) this.setState({ todos: newTodos }) }
|
知识点总结
- 拆分组件, 实现静态组件.
- 其中需要注意
className, style={{}} 的写法.
- 动态初始化列表
- 如何确认数据放在哪个组件中呢?
- 某个组件使用: 放在自身的 state 中即可
- 某些组件使用: 放在他们共同的父组件中即可 (这叫做状态提升)
- 子组件之间通信
- 父组件给子组件传递, 通过 props 传递
- 子组件给父组件传递, 通过 props 传递, 不过父组件需要给子组件传递函数
- 注意 defaultChecked (第一次起作用) 和 checked (一直有作用) 的区别, 类似的 defaultValue 和 value
- 状态在哪里, 操作状态的方法就在哪里