2023-06-02
React
00

目录

引言
一、拆分组件
二、实现静态组件(布局)
三、实现动态组件
🍎 1. 动态展示列表
🍍 2. 添加事项功能
🍋 3. 实现鼠标悬浮效果
🍉 4. 复选框状态维护
🍏 5. 限制参数类型
🍒 6. 删除按钮
🍓 7. 获取完成数量
🍊 8. 全选按钮
🥭 9. 删除已完成
总结

TodoList 案例

React 入门学习(六)-- TodoList 案例

📢 大家好,我是小超同学,一名在职的前端人员

📢 这篇文章是学习 React 练习中 TodoList 案例的操作笔记

📢 非常感谢你的阅读,不对的地方欢迎指正 🙏

📢 愿你忠于自己,热爱生活

引言

TodoList 案例在前端学习中挺重要的,从原生 JavaScript 的增删查改,到现在 React 的组件通信,都是一个不错的案例,这篇文章主要记录,还原一下通过 React 实现 TodoList 的全过程

功能:

  1. 显示所有todo列表
  2. 在输入框中输入文本、按下回车键把当前输入值显示到列表首位、并清除输入到文本
  3. 鼠标滑入每一个任务比如我鼠标移入到了吃饭这一项就高亮当前行、离开恢复正常
  4. 鼠标滑入每一个任务比如吃饭这一项后面要显示一个删除按钮
  5. 底部全选、已完成、全部要做动态展示与清楚已完成任务(就是选择了打勾的项)

https://image.myxuechao.com/blog/React/react-20.png

一、拆分组件

首先第一步需要做的是将这个页面拆分成几个组件

首先顶部的输入框,可以完成添加项目的功能,可以拆分成一个 Header 组件

中间部分可以实现一个渲染列表的功能,可以拆分成一个 List 组件

在这部分里面,每一个待办事项都可以拆分成一个 Item 组件

最后底部显示当前完成状态的部分,可以拆分成一个 Footer 组件

https://image.myxuechao.com/blog/React/react-21.png

在拆分完组件后把他们全部都引入到外部的父级App.jsx文件中,我们下一步要做的就是去实现这些组件的静态效果

二、实现静态组件(布局)

首先,我们可以先写好这个页面的静态页面,然后再分离组件,所以这就要求我们

以后写静态页面的时候,一定要有明确的规范

  1. 打好注释
  2. 每个部分的 CSS 要写各自文件的地方,不要随意写
  3. 命名一定要规范
  4. CSS 选择器不要关联太多层级
  5. 在写 HTML 时就要划分好布局

这样有利于我们分离组件

首先,我们在 src 目录下,新建一个 Components 文件夹,用于存放我们的组件,然后在文件夹下,新建 Header 、ItemList 、Footer 组件文件夹,再创建其下的 index.jsxindex.css 文件,用于创建对应组件及其样式文件

jsx
todolist ├─ package.json ├─ public │ ├─ favicon.ico │ └─ index.html ├─ src │ ├─ App.css │ ├─ App.jsx │ ├─ Components │ │ ├─ Footer │ │ │ ├─ index.css │ │ │ └─ index.jsx │ │ ├─ Header │ │ │ ├─ index.css │ │ │ └─ index.jsx │ │ ├─ Item │ │ │ ├─ index.css │ │ │ └─ index.jsx │ │ └─ List │ │ ├─ index.css │ │ └─ index.jsx │ └─ index.js └─ yarn.lock

最终目录结构如上

然后我们将每个组件,对应的 HTML 结构 写 到对应组件的 index.jsx 文件中 return 出来,再将 CSS 样式添加到 index.css 文件中

效果:

https://image.myxuechao.com/blog/React/react-22.png

记得在各自组件中,在 index.jsx 中一定要引入 index.css 文件

三、实现动态组件

🍎 1. 动态展示列表

我们目前实现的列表项是固定的,我们需要它通过状态来维护,而不是通过组件标签来维护

首先我们知道,父子之间传递参数,可以通过 state 和 props 实现

我们通过在父组件也就是 App.jsx 中设置状态

jsx
export default class App extends Component { // 初始化状态 state = { todos: [ { id: '001', name: '吃饭', done: true }, { id: '002', name: '睡觉', done: true }, { id: '003', name: '打代码', done: false }, { id: '004', name: '逛街', done: true } ] } render () { const { todos } = this.state return ( <div className="todo-container"> <div className="todo-wrap"> <Header></Header> <List></List> <Footer></Footer> </div> </div> ) } }

state数据再将它传递给对应的渲染组件 List

jsx
render () { const { todos } = this.state return ( <div className="todo-container"> <div className="todo-wrap"> <Header></Header> //传递数据给List <List todos={todos} ></List> <Footer></Footer> </div> </div> ) }

这样在 List 组件中就能通过 props 来获取到 todos

我们通过解构取出 todos

jsx
const { todos } = this.props

再通过 map 遍历渲染 Item 数量

jsx
{ todos.map(todo => { return <Item key={todo.id} {...todo}/> }) }

List组件效果如图:

https://image.myxuechao.com/blog/React/react-23.png

同时由于我们的数据渲染最终是在 Item 组件中完成的,所以我们需要将数据传递给 Item 组件

这里有两个注意点

  1. 关于 key 的作用在 diff 算法的文章中已经有讲过了,需要满足唯一性
  2. 这里采用了简写形式 {...todo} ,这使得代码更加简洁,它代表的意思是
jsx
id = {todo.id} name = {todo.name} done = {todo.done}

在 Item 组件中取出 props 即可使用

jsx
const { id, name, done } = this.props

Item组件效果如图:

https://image.myxuechao.com/blog/React/react-24.png

这样我们更改 APP.jsx 文件中的 state 就能驱动着 Item 组件的更新

同时这里需要注意的是

对于复选框的选中状态,这里采用的是 defaultChecked = {done},相比于 checked 属性,这个设定的是默认值,能够更改

🍍 2. 添加事项功能

首先我们需要在 Header 组件中,输入框绑定键盘事件,判断按下的是否为回车,如果为回车,则将当前输入框中的内容传递给 APP 组件

因为,在目前的学习知识中,Header 组件和渲染组件 List 属于兄弟组件,没有办法进行直接的数据传递,因此可以将数据传递给 APP 再由 APP 转发给 List。

jsx
//文件路径 Header/index.jsx // 键盘事件的回调 handelKeyUp = (event) => { // 解构赋值获取keyCode,target const { keyCode, target } = event // 判断是否是回车按键 if (keyCode !== 13) return // 不是回车键,直接返回 // 添加的todo名字不能为空 if (target.value.trim() === '') { alert('输入不能为空') return } // 准备好一个todo对象 const todoObj = { id: nanoid(), name: target.value, done: false } // 将todoObj传递给App this.props.addTodo(todoObj) // 清空输入 target.value = '' }

Header效果图

https://image.myxuechao.com/blog/React/react-25.png

我们在 App.jsx 中添加了事件 addTodo ,这样可以将 Header 组件传递的参数,维护到 App 的状态中

jsx
// App.jsx addTodo = (todoObj) => { const { todos } = this.state // 追加一个 todo const newTodos = [todoObj, ...todos] this.setState({ todos: newTodos }) }

App.jsx效果图

https://image.myxuechao.com/blog/React/react-26.png

在这小部分中,需要我们注意的是,我们新建的 todo 对象,一定要保证它的 id 的唯一性

这里采用的 nanoid 库,这个库的每一次调用都会返回一个唯一的值

jsx
npm i nanoid

安装这个库,然后引入

通过 nanoid() 即可生成唯一值

功能效果:

https://image.myxuechao.com/blog/React/react-27.gif

🍋 3. 实现鼠标悬浮效果

接下来我们需要实现每个 Item 中的小功能

首先是鼠标移入时的变色效果

我的逻辑是,通过一个状态来维护是否鼠标移入,比如用一个 mouse 变量,值给 false 当鼠标移入时,重新设定状态为 true 当鼠标移出时设为 false ,然后我们只需要在 style 中用mouse 去设定样式即可

下面我们来代码实现

在 Item 组件中,先设定状态

jsx
state = { mouse: false } // 标识鼠标移入,移出

给元素绑定上鼠标移入,移出事件

jsx
<li onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)} ><li/>

当鼠标移入时,会触发 onMouseEnter 事件,调用 handleMouse 事件传入参数 true 表示鼠标进入,更新组件状态

jsx
handleMouse = flag => { return () => { this.setState({ mouse: flag }) } }

再在 li 身上添加由 mouse 控制的背景颜色

jsx
style={{ backgroundColor: this.state.mouse ? '#ddd' : 'white' }}

同时通过 mouse 来控制删除按钮的显示和隐藏,做法和上面一样

jsx
<button className="btn btn-danger" style={{ display: mouse ? 'block' : 'none' }} >删除</button>

Item组件完整代码:

jsx
import React, { Component } from 'react' import './index.css' export default class Item extends Component { state = { mouse: false } // 标识鼠标移入、移出 // 鼠标移入移出的回调 handelMouse = (flag) => { return () => { this.setState({ mouse: flag }) } } render () { const { id, name, done } = this.props const { mouse } = this.state return ( <li style={{ backgroundColor: mouse ? '#ddd' : '#fff' }} onMouseLeave={this.handelMouse(false)} onMouseEnter={this.handelMouse(true)}> <label> <input type="checkbox" defaultChecked={done} /> <span>{name}</span> </label> </li> ) } }

功能效果:

https://image.myxuechao.com/blog/React/react-28.gif

🍉 4. 复选框状态维护

我们需要将当前复选框每一项的状态,维护到 state 当中

我们的思路是

在复选框中添加一个 onChange 事件来进行数据的传递,当事件触发时我们执行 handleCheck 函数,这个函数可以向 App 组件中传递参数,这样再在 App 中改变状态即可

首先绑定事件

jsx
// Item/index.jsx <input type="checkbox" defaultChecked={done} onChange={this.handleCheck(id)} />

事件回调

jsx
// Item/index.jsx handleCheck = (id) => { return (event) => { this.props.updateTodoDone(id, event.target.checked) } }

由于我们需要传递 id 来记录状态更新的对象,因此我们需要采用高阶函数的写法,不然函数会直接执行而报错,复选框的状态我们可以通过 event.target.checked 来获取

这样我们将我们需要改变状态的 Item 的 id 和改变后的状态,传递给了 App

内定义的updateTodoDone 事件,这样我们可以在 App 组件中操作改变状态

我们传递了两个参数 id 和 done

通过遍历找出该 id 对应的 todo 对象,更改它的 done 即可

jsx
// App.jsx // 用于更新一个todo对象 updateTodoDone = (id, done) => { // 获取状态中的todos const { todos } = this.state // 匹配处理数据 const newTodos = todos.map((todoObj) => { if (todoObj.id === id) return { ...todoObj, done } else return todoObj }) this.setState({ todos: newTodos }) }

然后App.js里的方法传递给List 组件

jsx
// App.jsx render () { const { todos } = this.state return ( <div className="todo-container"> <div className="todo-wrap"> <Header addTodo={this.addTodo} ></Header> //传递给 List组件 updateTodoDone函数 <List todos={todos} updateTodoDone={this.updateTodoDone} ></List> <Footer></Footer> </div> </div> ) }

因为Item组件在List组件中所以List组件在传递给Item组件

jsx
// List/index.jsx export default class List extends Component { render () { const { todos } = this.props return ( <ul className="todo-main"> { todos.map((todo) => { //传递给 Item组件 updateTodoDone函数 return <Item key={todo.id} {...todo} updateTodoDone={this.props.updateTodoDone} ></Item> })} </ul> ) } }

这里更改的方式是 { ...todoObj, done },首先会展开 todoObj 的每一项,再对 done 属性做覆盖(赋值的意思)

效果:

https://image.myxuechao.com/blog/React/react-29.gif

🍏 5. 限制参数类型

在我们前面写的东西中,我们并没有对参数的类型以及必要性进行限制

在前面我们也学过这个,我们需要借助 propTypes 这个库

jsx
npm i prop-types

首先我们需要引入这个库,然后对 props 进行限制

jsx
// Header import propTypes from 'prop-types' export default class Header extends Component { // 对接收的props进行:类型、必要性的限制 static propTypes = { addTodo: propTypes.func.isRequired, } // 键盘事件的回调 handelKeyUp = (event) => { ... } render () { return ( <div className="todo-header"> <input type="text" placeholder="请输入你的任务名称,按回车键确认" onKeyUp={this.handelKeyUp} /> </div> ) } }

在Header 组件中需要接收一个 addTodo 函数,所以我们进行一下限制

同时在 List 组件中也需要进行对 todos 以及 updateTodo 的限制

如果传入的参数不符合限制,则会报 warning

🍒 6. 删除按钮

现在我们需要实现删除按钮的效果

这个和前面的挺像的,首先我们分析一下,我们需要在 Item 组件上的按钮绑定点击事件,然后传入被点击事项的 id 值,通过 props 将它传递给父元素 List ,再通过在 List 中绑定一个 App 组件中的删除回调,将 id 传递给 App 来改变 state

首先我们先编写 点击事件然后绑定在点击事件的回调上

jsx
// Item/index.jsx export default class Item extends Component { state = { mouse: false } // 标识鼠标移入、移出 // 鼠标移入移出的回调 handelMouse = (flag) => { return () => { .... } } // 勾选、取消勾选某一个todo的回调 handelCheck = (id) => { return (event) => { ... } } // 删除一个todo的回调 handelDelete = (id) => { console.log('id: ', id); if (window.confirm('确定删除吗?')) { this.props.deleteTodo(id) } } render () { const { id, name, done } = this.props const { mouse } = this.state return ( <li style={{ backgroundColor: mouse ? '#ddd' : '#fff' }} onMouseLeave={this.handelMouse(false)} onMouseEnter={this.handelMouse(true)}> .... <button className="btn btn-danger" style={{ display: mouse ? 'block' : 'none' }} onClick={() => this.handelDelete(id)}>删除</button> </li> ) } }

子组件想影响父组件的状态,需要父组件传递一个函数,因此我们在 App 中添加一个 deleteTodo 函数

jsx
// app.jsx deleteTodo = (id) => { const { todos } = this.state const newTodos = todos.filter(todoObj => { return todoObj.id !== id }) this.setState({ todos: newTodos }) }

然后将这个deleteTodo函数传递给 List 组件,再传递给 Item

效果如何下:

https://image.myxuechao.com/blog/React/react-30.gif

🍓 7. 获取完成数量

我们在 App 中向 Footer 组件传递 todos 数据,再去统计数据

统计 done 为 true 的个数就是已完成

统计 todoslength 就是全部

jsx
//Foolter/index.jsx代码 export default class Footer extends Component { render () { // 获取todos的长度 const { todos } = this.props // 已完成的个数 const doneCount = todos.reduce((pre, todo) => { return pre + (todo.done ? 1 : 0) }, 0) // 总数 const total = todos.length return ( <div className="todo-footer"> <label> <input type="checkbox" /> </label> <span> <span>已完成{doneCount}</span> / 全部 {todos.length} </span> <button className="btn btn-danger" >清除已完成任务</button> </div> ) } }

效果:

https://image.myxuechao.com/blog/React/react-31.png

🍊 8. 全选按钮

首先我们需要在按钮上绑定事件,由于子组件需要改变父组件的状态,所以我们的操作和之前的一样,先绑定事件,再在 App 中传一个函数个 Footer ,再在 Footer 中调用这个函数并传入参数即可

这里需要特别注意的是

defaulChecked 只有第一次会起作用,所以我们需要将前面写的改成 checked 添加 onChange 事件即可

首先我们先在 App 中给 Footer 传入一个函数 checkAllTodo

jsx
// App.jsx checkAllTodo = (done) => { const { todos } = this.state const newTodos = todos.map((todoObj => { return { ...todoObj, done: done } })) this.setState({ todos: newTodos }) } // render <Footer todos={todos} checkAllTodo={this.checkAllTodo}/>

然后我们需要在 Footer组件全选的回调函数handelCheckedAll 中调用一下checkAllTodo

jsx
//Foolter/index.jsx // 全选checkbox的回调 handelCheckedAll = (event) => { this.props.checkAllTodo(event.target.checked) } <label> <input type="checkbox" checked={doneCount === total && total !== 0 ? true : false} onChange={this.handelCheckedAll} /> </label>

这里我们传入了一个参数:当前按钮的状态,用于全选和取消全选

同时我们需要排除总数为0 时的干扰

jsx
render () { // 获取todos的长度 const { todos } = this.props // 总数 const total = todos.length return ( <div className="todo-footer"> <label> <input type="checkbox" checked={doneCount === total && total !== 0 ? true : false} onChange={this.handelCheckedAll} /> </label> <span> </div> ) }

效果:

https://image.myxuechao.com/blog/React/react-32.gif

🥭 9. 删除已完成

给删除按钮添加一个点击事件,回调中调用 App 中添加的删除已完成的函数,全都一个套路

首先在 Footer 组件中定义删除的函数然后调用传来的函数

jsx
//Foolter/index.jsx // 清除已完成任务的回调 handelClearAllDone = () => { this.props.clearAllDone() } <button className="btn btn-danger" onClick={this.handelClearAllDone}>清除已完成任务</button>

在 App 中定义函数,过滤掉 done 为 true 的,再更新状态即可

jsx
// App.jsx clearAllDone = () => { const { todos } = this.state const newTodos = todos.filter((todoObj) => { return todoObj.done !== true }) this.setState({ todos: newTodos }) }

效果

https://image.myxuechao.com/blog/React/react-33.gif

总结

  1. 注意:className 、style 的写法
  2. 动态初始化列表,如何确定将数据放在哪个组件的 state 中?
  • 某个组件使用:放在其自身的 state 中
  • 某些组件使用:放在他们共同的父组件 state 中,即状态提升
  1. 关于父子之间通信:
  • 父传子:直接通过 props 传递
  • 子传父:父组件通过 props 给子组件传递一个函数,子组件调用该函数
  1. 注意 defaultChecked 和 checked 的区别
  2. 状态在哪里,操作状态的方法就在哪里
  3. 一定要自己敲一下,好好理解数据传递

非常感谢您的阅读,欢迎提出你的意见,有什么问题欢迎指出,谢谢!🎈

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:LiuXueChao

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!