React全家桶: TodoLis案例_详细拆解

1,167 阅读7分钟

「本文已参与低调务实优秀中国好青年前端社群的写作活动」。

写在前面

在最近看了React之后,一直觉得学的懵懵然,虽然很多大佬的手写笔记,写的都很不错,但是我一直没有我想要的那种细无巨细,比如类式组件this指向问题的追根溯源,又比如三大实例属性简写的由来,总之我还是决定做一份事无巨细的笔记。

那就让我们开始吧!

TodoList案例_静态组件

效果展示

demo2_todo list (4).gif

拆分组件: 拆分界面,抽取组件

  • Header、List、Item、Footer

实现静态组件: 使用组件实现静态页面效果

  • 将静态页面全部复制到App组件,进行详细拆分。

对class、fontsize、style等关键字进行替换为className、fontSize、style={{}}。

  • 创建App.css文件,将全部css样式引入,在App组件中引入样式文件。实现样式加工。

  • 将对应的html拆分到子组件内部,在App组件中引入然后使用。

注意:Item子组件在List子组件中使用时,使用几个Item子组件,就可以产生几个对应的html结构。

image.png

引入包的顺序

  • 第三方的包排序在前,自己写的包排序在后面。

样式拆分

  • 创建App.css文件,将全部css样式引入,在App组件中引入样式文件。实现样式加工。

  • 在每一个子组件文件夹下面创建index.css,将对应html的css样式代码拆分进index.css。(不用忘记在组件文件中引入index.css文件)

TodoList案例_动态初始化列表

父组件可以给子组件传递标签属性

兄弟组件不可以互相传递标签属性

解决方式

  • 将状态书写的共同的父组件内,在A子组件中展示状态数据,在B组件中通过调用函数更新状态。

在子组件中展示状态数据

  • App组件
//初始化状态
state = {todos:[
        {id:'001',name:'吃饭',done:true},
        {id:'002',name:'睡觉',done:true},
        {id:'003',name:'打代码',done:false},
        {id:'004',name:'逛街',done:false}
]}
  • List组件

使用展开运算符展开todo属性

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

做的事件勾选

  • 使用checked

直接写死,不可以切换。

  • 使用defaultchecked

可以切换打钩状态。

TodoList案例_添加todo

获取用户的输入

  • Header组件

1.为input标签绑定onKeyUp={this.handleKeyUp}

2.由于事件对象和操作对象是一样的,直接使用even.target

3.根据回车判断是否输出输入的值

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
		console.log(target.value);
	}

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

更新App组件的状态

实现的基本原理

  • 子组件调用函数(父组件通过props传递给子组件的函数)改变父组件的状态

image.png

1.在父组件中书写改变状态的函数,并且传递给子组件。

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

2.子组件调用函数,并且传递数值添加到父组件的状态中(注意传递的产生的一致)

这里传递给addTodo函数的参数是一个对象。

id值生成

1.使用时间戳、使用随机数

  1. 使用uuid库。生成唯一的标识符。(nanoid是一个比较迷你的库)

终端命令:

// 2选1
npm i nanoid

yarn add nanoid
  • 引入nanoid
import {nanoid} from 'nanoid' // 这里的nanoid是一个函数
  • 添加一个括号进行调用
id:nanoid()
  • Header组件部分代码
//键盘事件的回调
handleKeyUp = (event)=>{
        //解构赋值获取keyCode,target
        const {keyCode,target} = event
        //判断是否是回车按键
        if(keyCode !== 13) return
        //准备好一个todo对象
        const todoObj = {id:nanoid(),name:target.value,done:false}
        //将todoObj传递给App
        this.props.addTodo(todoObj)
}
  • 实现效果

image.png

修复bug

  1. 添加的todo名字不能为空
//添加的todo名字不能为空
if(target.value.trim() === ''){
        alert('输入不能为空')
        return
}
  1. 清空输入框中的输入
//清空输入
target.value = ''

TodoList案例_鼠标移入效果

给鼠标绑定事件

  • 给Item组件绑定onMouseEnter、onMouseLeave
//鼠标移入、移出的回调
handleMouse = (flag)=>{
        return ()=>{
        // 修改状态
                this.setState({mouse:flag})
        }
}
        
onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)
  • 注意这里的handleMouse使用高阶函数。

  • 根据状态的改变进行样式的转换

// item组件
style={{backgroundColor:mouse ? '#ddd' : 'white'}}

// 删除按钮的展示
style={{display:mouse?'block':'none'}}
  • 效果展示

image.png

TodoList案例_添加—个todo

改变事件是否勾选的状态

  • 为Item组件绑定onCHange事件
<input type="checkbox" checked={done} onChange={this.handleCheck(id)}/>

因为绑定事件和要操作的事件是同一个,直接使用event.target而不是使用ref属性。

type="checkbox"这个类型的input标签有一个属性event.target.checked可以获得是否被勾选。

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

image.png

  • update函数(App组件内部)

通过参数id找到对应的对象,并且修改该对象的其他属性(这里是done)。进而达到修改状态的目的。

//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})
}
  • 传递函数给子组件List组件、List组件传递给子组件Item组件
<List todos={todos} updateTodo={this.updateTodo} />

<Item key={todo.id} {...todo} updateTodo={updateTodo} />

在Item组件中调用该函数

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

状态在哪里,操作状态的方法就在哪里(父组件)

调用函数(子组件)

TodoList案例_对props进行限制

引入Props-Type库

  • 下载库命名
yarn add prop-types
  • 引入库到Header组件
import PropTypes from 'prop-types'
  • 对传入子组件的props进行限制
  1. Header组件
//对接收的props进行:类型、必要性的限制
static propTypes = {
        addTodo:PropTypes.func.isRequired
}
  1. List组件
//对接收的props进行:类型、必要性的限制
static propTypes = {
        todos:PropTypes.array.isRequired,
        updateTodo:PropTypes.func.isRequired,
        deleteTodo:PropTypes.func.isRequired,
}
  1. Item组件
//对接收的props进行:类型、必要性的限制
static propTypes = {
        todos:PropTypes.array.isRequired,
        updateTodo:PropTypes.func.isRequired,
        deleteTodo:PropTypes.func.isRequired,
}

TodoList案例_删除一个todo

父组件

  • 父组件书写一个deleteTodo函数,将状态修改
//deleteTodo用于删除一个todo对象
deleteTodo = (id)=>{
        //获取原来的todos
        const {todos} = this.state
        //删除指定id的todo对象
        const newTodos = todos.filter((todoObj)=>{
                return todoObj.id !== id
        })
        //更新状态
        this.setState({todos:newTodos})
}

子组件

  • 子组件绑定点击事件调用deleteTodo
//删除一个todo的回调
handleDelete = (id)=>{
        // 一个删除提示框
        if(window.confirm('确定删除吗?')){
                this.props.deleteTodo(id)
        }
}

TodoList案例_实现底部功能

数组API reduce

  • 传送门

developer.mozilla.org/zh-CN/docs/…

  • 2个参数

第一个参数是回调函数,第二个是初始值。

  • 使用reduce遍历数组统计完成个数
const doneCount = todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0),0)
  • 所有事件总数
//总数
const total = todos.length

完成个数等于总数

  • 对Footer组件进行打钩。

1.不能使用defaultchecked (一次性)

页面渲染的第一次,defaultchecked就会被赋值。在之后的状态改变导致完成个数等于总数的时候也不会改变defaultchecked的值。

  1. 使用checked会报错。(写死)

image.png

  • 添加onchange事件调用checkAllTodo函数修改状态
  1. App组件
//checkAllTodo用于全选
checkAllTodo = (done)=>{
        //获取原来的todos
        const {todos} = this.state
        //加工数据
        const newTodos = todos.map((todoObj)=>{
                return {...todoObj,done}
        })
        //更新状态
        this.setState({todos:newTodos})
}

2.Footer组件

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

  1. 要将Item组件中使用的defaultchecked修改为checked。
<input type="checkbox" checked={done} onChange={this.handleCheck(id)}/>

修补bug

  • 删除全部Item组件之后,未消除打钩

image.png

  • 解决手段

在给打钩添加一个条件(事件总数不为0)

<input type="checkbox" onChange={this.handleCheckAll} checked={doneCount === total && total !== 0 ? true : false}/>

清除全部完成事件

为删除完成事件按钮添加点击事件

  1. 在父组件中书写删除全部完成事件的函数
//clearAllDone用于清除所有已完成的
clearAllDone = ()=>{
        //获取原来的todos
        const {todos} = this.state
        //过滤数据
        const newTodos = todos.filter((todoObj)=>{
                return !todoObj.done
        })
        //更新状态
        this.setState({todos:newTodos})
}

注意: 数组API filter

  • 传送门

developer.mozilla.org/zh-CN/docs/…

  1. 在Footer组件中使用点击事件回调clearAllDone函数
//清除已完成任务的回调
handleClearAllDone = ()=>{
        this.props.clearAllDone()
}

TodoList案例_总结

1.拆分组件、实现静态组件,注意:className、style的写法

2.动态初始化列表,如何确定将数据放在哪个组件的state中?

    ——某个组件使用:放在其自身的state中
    
    ——某些组件使用:放在他们共同的父组件state中(官方称此操作为:状态提升)
    

3.关于父子之间通信:

        1.【父组件】给【子组件】传递数据:通过props传递
        
        2.【子组件】给【父组件】传递数据:通过props传递,要求父提前给子传递一个函数
        

4.注意defaultChecked 和 checked的区别,类似的还有:defaultValue 和 value

5.状态在哪里,操作状态的方法就在哪里