使用React实现一个简单的待办事项列表 | 青训营

一、介绍

主要功能

用户可以添加、编辑和删除待办事项。

效果展示

这篇文章我们将详细讲解如何建立一个这样简单的列表。

二、编码

(一)搭建项目、划分组件

第一步 ,使用npm i -g create-react-app全局安装脚手架。

第二步 ,使用create-react-app staying-to-do创建一个叫staying-to-do的项目文件,这里的文件名字大家可以自由定义。

注意: 这里自定义的文件名不要包含大写,不然会报错。

第三步 ,进入项目文件夹:cd hello-react

第四步 ,启动项目:npm start

第四步完成之后会自动打开一个react页面,页面中会有一个一个旋转的react大loge就算启动成功了!

第五步 划分组件。

总所周知,react最重要的就是组件,这里很显然是添加待办的头部、统计和删除的底部、具体展示的中部列表和列表中的每个小展示条,这四个组件的文件夹分别命名为HeaderFooterListItem。如下图所示:

每个组件都是由一个index.jsxindex.css构成,把所有组件都放在一个叫components的文件夹中,再把components放到文件夹src方便以后查找,最后形成的项目目录应该如下:

当然,我这样的做法并不是一定的,大家可以根据自己的思路划分组件。

(二)分析思路、直接开干

传值方式

很显然,这个项目最有练习意义的就是组件之间的传值,例如Header组件输入的数据要在Item组件中展现,也就是要实现兄弟组件之间的传值。我们都知道利用组件三大属性中的stateprops就能实现父子组件之间的传值,但是如何实现兄弟组件之间的传值呢,或许大家都有学过一些消息订阅啊hook的方法,但是这里我将教大家用最原始的方法实现,或许这不是在世纪开发中最常用的方法,但一定能在初学之时帮助我们更好的练习react的相关特性。

我的方法是把要使用的数据放在所有组件的父亲------App组件中,然后利用stateprops传给子组件,这样就用父子组件传值实现了兄弟组件的传值。使用这个方法,父亲组件要预先使用props属性向子组件传递一个函数,子组件在调用这个函数获取父亲组件保存的数据(也就是俗称的 状态在哪里,操作状态的方法就在哪里

开始编写

App.jsx文件中,

第一,用一个todos的保存待办事项,每个元素都是一个对象,每个对象包括待办事项的序号、待办事项的名称和是否完成的标志。

第二,引入要用的组件,例如import Footer from './components/Footer'。布局,把组件放在该放的地方。引入全局样式App.css,里面具体写啥咱后面再说

第三,分析全局要实现的功能,定义相关的函数,然后把函数用props传给子组件,供子组件使用。例如Header组件有一个输入回车添加待办事件的功能,本质上就是接受一个新的对象,然后更新状态里的todos,我们把这个函数addTodo在App.jsx中定义好,然后传递给子组件使用<Header addTodo={this.addTodo}/>

所以,整个App.jsx的代码如下:

js 复制代码
import React, { Component } from 'react'
import Header from './components/Header'
import List from './components/List'
import Footer from './components/Footer'
import './App.css'

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:false}
	]}

	//addTodo用于添加一个todo,接收的参数是todo对象
	addTodo = (todoObj)=>{
		//获取原todos
		const {todos} = this.state
		//追加一个todo
		const newTodos = [todoObj,...todos]
		//更新状态
		this.setState({todos:newTodos})
	}

	//updateTodo用于更新一个todo对象
	updateTodo = (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})
	}

	//deleteTodo用于删除一个todo对象
	deleteTodo = (id)=>{
		//获取原来的todos
		const {todos} = this.state
		//删除指定id的todo对象
		const newTodos = todos.filter((todoObj)=>{
			return todoObj.id !== id
		})
		//更新状态
		this.setState({todos:newTodos})
	}

	//checkAllTodo用于全选
	checkAllTodo = (done)=>{
		//获取原来的todos
		const {todos} = this.state
		//加工数据
		const newTodos = todos.map((todoObj)=>{
			return {...todoObj,done}
		})
		//更新状态
		this.setState({todos:newTodos})
	}

	//clearAllDone用于清除所有已完成的
	clearAllDone = ()=>{
		//获取原来的todos
		const {todos} = this.state
		//过滤数据
		const newTodos = todos.filter((todoObj)=>{
			return !todoObj.done
		})
		//更新状态
		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} deleteTodo={this.deleteTodo}/>
					<Footer todos={todos} checkAllTodo={this.checkAllTodo} clearAllDone={this.clearAllDone}/>
                            </div>
			</div>
		)
	}
}

如你所见,这里除了addTodo函数,还有Footer用于统计数据的checkAllTodo和清除按钮要用的clearAllDone,还有Item要用的更新的updateTodo和删除按钮要用的deleteTodo函数,只不过特殊的是ItemList的子组件,需要通过List获取罢了。

Header组件中

index.jsx中,定义好一个输入框之后最重要的就是获取到输入的内容构建一个对象,然后传给父组件传来的addTodo,实现这个功能的函数我们叫handleKeyUp,这个函数在onKeyUp按键弹起后触发,event中的target.valuekeyCode可以用于获取输入值和某一案件的键值。具体代码如下:

js 复制代码
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import {nanoid} from 'nanoid'
import './index.css'

export default class Header extends Component {

	//对接收的props进行:类型、必要性的限制
	static propTypes = {
		addTodo:PropTypes.func.isRequired
	}

	//键盘事件的回调
	handleKeyUp = (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 = ''
	}

	render() {
		return (
			<div className="todo-header">
				<input onKeyUp={this.handleKeyUp} type="text" placeholder="请输入你的代办事件名称,按回车键确认"/>
			</div>
		)
	}
}

在这里我们对父组件App.jsx传过来的addTodo进行类型限制,这里限制为func函数型,这里我们需要使用npm install prop-types额外在下载一个库prop-types来限制。

在构建对象的时候需要一个唯一的id,这里使用npm install nanoid安装nanoid,nanoid 是一个生成唯一标识符(UUID)的函数。它使用随机算法生成短字符串,可以用于为应用程序中的唯一标识符生成惟一的、不可预测的值。

List组件中

index.jsx中要接收到父组件App.jsx传来的todos,updateTodo,deleteTodo,然后引入子组件Item,再根据todos中的每个对象的id来渲染多少个Item,代码如下:

js 复制代码
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Item from '../Item'
import './index.css'

export default class List extends Component {

	//对接收的props进行:类型、必要性的限制
	static propTypes = {
		todos:PropTypes.array.isRequired,
		updateTodo:PropTypes.func.isRequired,
		deleteTodo:PropTypes.func.isRequired,
	}

	render() {
		const {todos,updateTodo,deleteTodo} = this.props
		return (
			<ul className="todo-main">
				{
				    todos.map( todo =>{
					return <Item key={todo.id} {...todo} updateTodo={updateTodo} deleteTodo={deleteTodo}/>
				    })
				}
			</ul>
		)
	}
}

Item组件中

Item组件中有一个很明显的效果就是经过哪一个Item哪一个Item就有一个高亮的效果,所以这里我们需要定义一个状态mouse来判断鼠标是否经过某一Item,默认为false不经过,因此需要定义一个由鼠标进入和鼠标退出触发的方法handleMouse来改变鼠标的状态,index.jsx代码如下:

js 复制代码
import React, { Component } from 'react'
import './index.css'

export default class Item extends Component {

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

	//鼠标移入、移出的回调
	handleMouse = (flag)=>{
		return ()=>{
			this.setState({mouse:flag})
		}
	}

	//勾选、取消勾选某一个todo的回调
	handleCheck = (id)=>{
		return (event)=>{
			this.props.updateTodo(id,event.target.checked)
		}
	}

	//删除一个todo的回调
	handleDelete = (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' : 'white'}} onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}>
				<label>
					<input type="checkbox" checked={done} onChange={this.handleCheck(id)}/>
					<span>{name}</span>
				</label>
				<button onClick={()=> this.handleDelete(id) } className="btn btn-blue" style={{display:mouse?'block':'none'}}>删除</button>
			</li>
		)
	}
}

每次勾选Item中的框框都要改变父组件App中的todos中对象的done属性,这个方法叫handleCheck,由输入框的是否改变事件触发;每次点击Item末尾的按钮都会触发handleDelete这个方法,它会把当前Item的id传给父组件的deleteTodo方法,通过这种方法删除父组件中的todos里的对象。

Footer组件中

待办事件总数我们直接用父组件App.jsx传递过来的todos的长度来表示, index.jsx代码如下:

js 复制代码
import React, { Component } from 'react'
import './index.css'

export default class Footer extends Component {

	//全选checkbox的回调
	handleCheckAll = (event)=>{
		this.props.checkAllTodo(event.target.checked)
	}

	//清除已完成任务的回调
	handleClearAllDone = ()=>{
		this.props.clearAllDone()
	}

	render() {
		const {todos} = this.props
		//已完成的个数
		const doneCount = todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0),0)
		//总数
		const total = todos.length
		return (
			<div className="todo-footer">
				<label>
					<input type="checkbox" onChange={this.handleCheckAll} checked={doneCount === total && total !== 0 ? true : false}/>
				</label>
				<span>
					<span>已完成{doneCount}</span> / 全部{total}
				</span>
				<button onClick={this.handleClearAllDone} className="btn btn-blue">清除已完成事件</button>
			</div>
		)
	}
}

这段代码中想必大家看不懂的就只有const doneCount = todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0),0)这行,其实这段代码没什么了不起的,就是JavaScript中会用到的,下面我详细讲解一下:

  1. todos:是一个任务列表数组,每个任务都有一个 done 属性来表示任务是否已完成。
  2. reduce:是一个数组方法,用于对数组的每个元素进行累积计算。它接受两个参数:回调函数和初始值。
  3. (pre, todo) => pre + (todo.done ? 1 : 0):这是一个回调函数,用于定义每个元素的累积计算规则。它接受两个参数:累积值(pre)和当前元素(todo)。对于每个元素,如果 todo.done 为真(即任务已完成),则返回 pre + 1,否则返回 pre
  4. 0:这是 reduce 方法的初始值,用于设置初始的累积值。

通过将每个已完成的任务计算为 1,未完成的任务计算为 0,然后累加所有元素的计算结果,即可得到已完成任务的数量。最后,将计算结果赋值给 doneCount 变量。

注意事项

我并没有把代码完全展现在文章中,例如每个组件的index.css和全局的App.css,这些是CSS的内容,大家就自己变着花样写吧。

三、总结

这个小案例我们很好的实现了一个可以添加,删除和编辑的待办事项的案例,很好的练习了react的两大属性stateprops,学会了 prop-types 和 nanoid 组件库,可见收获多多。

相关推荐
Find23 天前
MaxKB 集成langchain + Vue + PostgreSQL 的 本地大模型+本地知识库 构建私有大模型 | MarsCode AI刷题
青训营笔记
理tan王子23 天前
伴学笔记 AI刷题 14.数组元素之和最小化 | 豆包MarsCode AI刷题
青训营笔记
理tan王子23 天前
伴学笔记 AI刷题 25.DNA序列编辑距离 | 豆包MarsCode AI刷题
青训营笔记
理tan王子23 天前
伴学笔记 AI刷题 9.超市里的货物架调整 | 豆包MarsCode AI刷题
青训营笔记
夭要7夜宵25 天前
分而治之,主题分片Partition | 豆包MarsCode AI刷题
青训营笔记
三六1 个月前
刷题漫漫路(二)| 豆包MarsCode AI刷题
青训营笔记
tabzzz1 个月前
突破Zustand的局限性:与React ContentAPI搭配使用
前端·青训营笔记
Serendipity5651 个月前
Go 语言入门指南——单元测试 | 豆包MarsCode AI刷题;
青训营笔记
wml1 个月前
前端实践-使用React实现简单代办事项列表 | 豆包MarsCode AI刷题
青训营笔记
用户44710308932421 个月前
详解前端框架中的设计模式 | 豆包MarsCode AI刷题
青训营笔记