【第三章-react 应用(基于 react 脚手架)】
- [一、使用 create-react-app 创建 react 应用](#一、使用 create-react-app 创建 react 应用)
-
- [1. react 脚手架](#1. react 脚手架)
- [2. 创建项目并启动](#2. 创建项目并启动)
- [3. react 脚手架项目结构](#3. react 脚手架项目结构)
- [4. 一个简单的 Hello 组件](#4. 一个简单的 Hello 组件)
- [5. vscode中 react 插件的安装](#5. vscode中 react 插件的安装)
- 二、组件的组合使用
-
- [1. 功能界面的组件化编码流程(通用)](#1. 功能界面的组件化编码流程(通用))
- [2. 组件的组合使用------TodoList案例](#2. 组件的组合使用——TodoList案例)
-
- [2.1 拆分组件](#2.1 拆分组件)
- [2.2 完成静态页面](#2.2 完成静态页面)
- [2.3 动态初始化列表](#2.3 动态初始化列表)
- [2.4 添加一个todo](#2.4 添加一个todo)
- [2.5 鼠标移入效果(背景色高亮并显示删除按钮)](#2.5 鼠标移入效果(背景色高亮并显示删除按钮))
- [2.6 勾选或取消勾选某一个todo](#2.6 勾选或取消勾选某一个todo)
- [2.7 对 props 进行限制](#2.7 对 props 进行限制)
- [2.8 删除一个 todo](#2.8 删除一个 todo)
- [2.9 实现底部功能](#2.9 实现底部功能)
- [2.10 总结 TodoList 案例](#2.10 总结 TodoList 案例)
一、使用 create-react-app 创建 react 应用
1. react 脚手架
1.脚手架: 用来帮助程序员快速创建一个基于 xxx 库的模板项目;
- 包含了所有需要的配置(语法检查、
jsx编译、devServer...); - 下载好了所有相关的依赖;
- 可以直接运行一个简单效果;
2.react 提供了一个用于创建 react 项目的脚手架库: create-react-app;
3.项目的整体技术架构为:react + webpack + es6 + eslint;
4.使用脚手架开发的项目的特点:模块化,组件化,工程化。
2. 创建项目并启动
- 第一步,全局安装:
npm install -g create-react-app - 第二步,创建项目hello-react:
create-react-app react-staging - 第三步,进入项目文件夹:
cd hello-react - 第四步,启动项目:
npm start
创建项目成功:

页面效果:

3. react 脚手架项目结构
- public ---- 静态资源文件夹
favicon.icon ------ 网站页签图标(偏爱图标)
index.html --------主页面
logo192.png ------- logo 图
logo512.png ------- logo 图
manifest.json ----- 应用加壳的配置文件
robots.txt -------- 爬虫协议文件
其中最重要的是主页面index.html:
javascript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- %PUBLIC_URL% 代表public文件夹的路径 -->
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!-- 开启理想视口,用于做移动端网页的适配 -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- 用于配置浏览器页签+地址栏的颜色(仅支持安卓手机浏览器) -->
<meta name="theme-color" content="#000000" />
<!-- 描述网站信息 -->
<meta
name="description"
content="Web site created using create-react-app"
/>
<!-- 用于指定网页添加到手机主屏幕后的图标 -->
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- 应用加壳时的配置文件 -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<!-- 若浏览器不支持js,则展示标签中的内容 -->
<noscript>You need to enable JavaScript to run this app.</noscript>
<!-- 相当于之前的test容器 -->
<div id="root"></div>
</body>
</html>
- src ----源码文件夹
App.css --------App 组件的样式
App.js --------- App 组件
App.test.js ---- 用于给 App 做测试
index.css ------ 样式
index.js ------- 入口文件
logo.svg ------- logo 图
reportWebVitals.js ------- 页面性能分析文件(需要 web-vitals 库的支持)
setupTests.js ------- 组件测试(需要 jest-dom 库的支持)
上面最重要的三个文件分别是:
- index.html:主页面
- index.js:入口文件
- app.js:App 组件
4. 一个简单的 Hello 组件
效果:在页面输出一句话"Hello,React"

- 清空
public和src文件夹,并在public文件夹新建index.html,新建root节点;
javascript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>hello,react</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
- 在
src文件夹创建入口文件index.js和组件App.js(组件名字要大写);
入口文件index.js需要引入React,ReactDom并渲染App组件挂载在root上:
javascript
//引入react核心库
import React from 'react'
//引入ReactDOM
import ReactDOM from 'react-dom/client'
//引入App组件
import App from './App'
// 渲柒App到页面
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App/>);
组件App.js里面只需要创建一个类式组件并暴露即可:
javascript
//创建"外壳"组件App
import React from 'react'
//定义并暴露App组件
class App extends React.Component {
render() {
return (
<h1>
Hello,React
</h1>
)
}
}
这样在页面就可以展示"Hello,React"了!

但这样的写法并不规范,我们需要创建Hello组件,并在App.js中引入使用。
React.Component的两种优化方式:
第一种:解构赋值
javascript
import React from 'react'
// 解构赋值
const {Component} = React;
class App extends Component {
...
}
第二种:利用模块化语法引入Component
javascript
import React, {Component} from 'react'
class App extends Component {
...
}
我们来模拟下react中的写法,你就会明白这个地方的使用:
javascript
let React = {
a:1,
b:2
}
// 这个地方暴露了Component,所以我们才能通过 import {Component} from 'react' 的方式引入
export class Component {
}
React.Component = Component;
export default React;
App是外壳,要利用组件化的方式编写里面的内容;
同级目录下,创建组件Hello.js:
javascript
import React, {Component} from 'react'
export default class Index extends Component {
render() {
return (
<h1>Hello,React</h1>
)
}
}
在App组件中引入Hello.js并使用:
javascript
//创建"外壳"组件App
import React, {Component} from 'react'
import Hello from './Hello'
//创建并暴露App组件
export default class App extends Component {
render() {
return (
<div>
<Hello/>
</div>
)
}
}
- 如果组件很多,放在
App.js和入口文件index.js的同级目录里会混乱,所以我们创建一个components文件夹,专门用来存放组件文件;
通常我们的每个组件会对应一个文件夹,文件夹的名字一般以组件的名字命名(首字母大写),该文件夹里面有两个文件:index.jsx 和 index.css
index.jsx:组件代码(也就是我们的Hello.js);index.css:组件样式;
注意:组件以.jsx后缀结尾,是为了方便我们区分该文件是组件 还是业务逻辑的js文件 。
我们创建一个Hello文件夹,放在components文件夹下面:

Hello/index.jsx代码如下:
javascript
import React, {Component} from 'react'
// 引入样式文件
import './index.css'
export default class Index extends Component {
render() {
return (
<h1 className="title">Hello,React</h1>
)
}
}
Hello/index.css代码如下:
javascript
.title {
background-color: orange;
}
- 这样写的优势就在于,如果你还有一个组件叫
Welcome,那么你还可以继续写下去;
创建Welcome文件夹:

Welcome/index.jsx:
javascript
import React, {Component} from 'react'
import './index.css'
export default class Welcome extends Component {
render() {
return (
<h2 className="desc">Welcome</h2>
)
}
}
Welcome/index.css:
javascript
.desc {
background-color: pink;
}
App.js:
javascript
//创建"外壳"组件App
import React, {Component} from 'react'
// const {Component} = React;
import Hello from './components/Hello'
import Welcome from './components/Welcome'
//创建并暴露App组件
export default class App extends Component {
render() {
return (
<div>
<Hello/>
<Welcome/>
</div>
)
}
}
此时页面效果:

- 样式的模块化;
如果Hello组件和Welcome组件都有title这个类名的时候,它们的样式最后汇总到App.js中就会发生冲突,一般有两种解决方案:
方案一:使用scss或者less文件,在容器最外层定义父类名;
Hello/index.jsx:
javascript
import React, {Component} from 'react'
import './index.css'
export default class Index extends Component {
render() {
return (
<div className="hello">
<h1 className="title">Hello,React</h1>
</div>
)
}
}
Hello/index.scss:
javascript
.hello {
.title {
background-color: orange;
}
}
方案二:样式的模块化;
先将index.css命名为index.module.css:

在Hello/index.jsx中以模块化的方式引入:
javascript
import React, {Component} from 'react'
// 以模块化的方式引入样式,此时 hello 是个对象
import hello from './index.module.css'
export default class Index extends Component {
render() {
return (
<h1 className={hello.title}>Hello,React</h1>
)
}
}
但是一般在我们的日常开发中,采用更多的还是使用scss或者less的方法写,然后形成嵌套关系。
5. vscode中 react 插件的安装
- 插件市场搜索"
react",找到ES7+ React/Redux/React-Native snippets;

- 一些常用的代码片段(代码模板);
rcc:创建一个类组件;

rfc:创建一个函数组件;

imp:引入模块;

更多的代码片段的使用请参考:vscode-react-javascript-snippets
二、组件的组合使用
1. 功能界面的组件化编码流程(通用)
-
拆分组件:拆分界面,抽取组件;
-
实现静态组件:使用组件实现静态页面效果;
-
实现动态组件:
- 3.1 动态显示初始化数据;
-- 数据类型;
-- 数据名称;
-- 保存在哪个组件? - 3.2 交互(从绑定事件监听开始);

2. 组件的组合使用------TodoList案例
功能:组件化实现此列表。
1显示所有todo列表;
2.输入文本,点击回车按钮,文本添加到列表中的首位,同时清除输入框中的文本;
3.鼠标悬浮每一列todo上,高亮并展示删除按钮,点击删除按钮可以对每一列todo进行删除;
4.底部对已完成todo和总数进行计数,并可以全部勾选、全部取消勾选,清除已完成todo;

2.1 拆分组件

建立组件文件夹:

2.2 完成静态页面
- 我们使用
Gemini生成了一份html和css的文件;
index.html:
javascript
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>任务列表</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="todo-container">
<div class="input-section">
<input type="text" placeholder="请输入你的任务名称,按回车键确认">
</div>
<ul class="todo-list">
<li>
<input type="checkbox">
<span>xxxxx</span>
</li>
<li>
<input type="checkbox">
<span>yyyyy</span>
</li>
</ul>
<div class="footer-section">
<div class="stats">
<input type="checkbox">
<span>已完成0 / 全部2</span>
</div>
<button class="clear-btn">清除已完成任务</button>
</div>
</div>
</body>
</html>
style.css:
javascript
/* 基础样式重置 */
body {
font-family: "Microsoft YaHei", Arial, sans-serif;
background-color: #f5f5f5;
display: flex;
justify-content: center;
padding-top: 50px;
}
/* 外层容器 */
.todo-container {
width: 500px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* 输入框样式 */
.input-section input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box; /* 确保padding不撑开宽度 */
outline: none;
font-size: 14px;
}
.input-section input:focus {
border-color: #4a90e2;
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6);
}
/* 列表样式 */
.todo-list {
list-style: none;
padding: 0;
margin: 15px 0 0 0;
border: 1px solid #ddd;
border-radius: 4px;
}
.todo-list li {
padding: 10px;
border-bottom: 1px solid #ddd;
display: flex;
align-items: center;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li input[type="checkbox"] {
margin-right: 10px;
}
/* 底部区域 */
.footer-section {
margin-top: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.stats {
display: flex;
align-items: center;
font-size: 14px;
}
.stats input {
margin-right: 15px;
}
/* 红色清除按钮 */
.clear-btn {
background-color: #da5a47;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.clear-btn:hover {
background-color: #c04d3c;
}
- 将生成的
html结构复制到App.jsx中,并将class替换成className,style改写成双花括号的格式style={``{}};
App.jsx:
javascript
import React, {Component} from 'react'
export default class App extends Component {
render() {
return (
<div className="todo-container">
<div className="input-section">
<input type="text" placeholder="请输入你的任务名称,按回车键确认"/>
</div>
<ul className="todo-list">
<li>
<input type="checkbox"/>
<span>xxxxx</span>
</li>
<li>
<input type="checkbox"/>
<span>yyyyy</span>
</li>
</ul>
<div className="footer-section">
<div className="stats">
<input type="checkbox"/>
<span>已完成0 / 全部2</span>
</div>
<button className="clear-btn">清除已完成任务</button>
</div>
</div>
)
}
}
- 将生成的
css样式复制到App.css中,并在App.jsx中引入;
App.css:
javascript
/* 基础样式重置 */
body {
font-family: "Microsoft YaHei", Arial, sans-serif;
background-color: #f5f5f5;
display: flex;
justify-content: center;
padding-top: 50px;
}
/* 外层容器 */
.todo-container {
width: 500px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* 输入框样式 */
.input-section input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box; /* 确保padding不撑开宽度 */
outline: none;
font-size: 14px;
}
.input-section input:focus {
border-color: #4a90e2;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6);
}
/* 列表样式 */
.todo-list {
list-style: none;
padding: 0;
margin: 15px 0 0 0;
border: 1px solid #ddd;
border-radius: 4px;
}
.todo-list li {
padding: 10px;
border-bottom: 1px solid #ddd;
display: flex;
align-items: center;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li input[type="checkbox"] {
margin-right: 10px;
}
/* 底部区域 */
.footer-section {
margin-top: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.stats {
display: flex;
align-items: center;
font-size: 14px;
}
.stats input {
margin-right: 15px;
}
/* 红色清除按钮 */
.clear-btn {
background-color: #da5a47;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.clear-btn:hover {
background-color: #c04d3c;
}
App.jsx:
javascript
import './App.css'
- 抽取Header组件;
Header/index.jsx:
javascript
import React, {Component} from 'react';
import './index.css'
class Header extends Component {
render() {
return (
<div className="header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认"/>
</div>
);
}
}
export default Header;
Header/index.css:
javascript
/* 输入框样式 */
.header input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box; /* 确保padding不撑开宽度 */
outline: none;
font-size: 14px;
}
.header input:focus {
border-color: #4a90e2;
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6);
}
- 抽取 List 和 Item 组件;
List/index.jsx:
javascript
import React, {Component} from 'react';
import Item from './components/Item';
import './index.css'
class List extends Component {
render() {
return (
<ul className="todo-list">
<Item/>
</ul>
);
}
}
export default List;
List/index.css:
javascript
/* 列表样式 */
.todo-list {
list-style: none;
padding: 0;
margin: 15px 0 0 0;
border: 1px solid #ddd;
border-radius: 4px;
}
.todo-list li {
padding: 10px;
border-bottom: 1px solid #ddd;
display: flex;
align-items: center;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li input[type="checkbox"] {
margin-right: 10px;
}
Item/index.jsx:
javascript
import React, {Component} from 'react';
import './index.css'
class Item extends Component {
render() {
return (
<li>
<input type="checkbox"/>
<span>xxxxx</span>
</li>
);
}
}
export default Item;
Item/index.css:
javascript
.item {
position: relative;
}
.clear-btn.right {
position: absolute;
right: 10px;
}
.item .clear-btn {
padding: 6px 10px;
font-size: 12px;
}
- 抽取Footer组件;
Footer/index.jsx:
javascript
import React, {Component} from 'react';
import './index.css'
class Footer extends Component {
render() {
return (
<div className="footer-section">
<div className="stats">
<input type="checkbox"/>
<span>已完成0 / 全部2</span>
</div>
<button className="clear-btn">清除已完成任务</button>
</div>
);
}
}
export default Footer;
Footer/index.css:
javascript
/* 底部区域 */
.footer {
margin-top: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.stats {
display: flex;
align-items: center;
font-size: 14px;
}
.stats input {
margin-right: 15px;
}
/* 红色清除按钮 */
.clear-btn {
background-color: #da5a47;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.clear-btn:hover {
background-color: #c04d3c;
}
App.jsx:
javascript
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 {
render() {
return (
<div className="todo-container">
<Header/>
<List/>
<Footer/>
</div>
)
}
}
页面效果:

2.3 动态初始化列表
- 由于
Header,List和Footer组件都需要用到数据,所以我们把数据放到App.jsx的state中,然后在通过props的方式传递给各个子组件。
App.jsx:
javascript
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 = {
toDoList: [{
id: '1',
name: '吃饭',
done: true
}, {
id: '2',
name: '睡觉',
done: true
}, {
id: '3',
name: '打豆豆',
done: false
}]
}
render() {
return (
<div className="todo-container">
<Header/>
<List toDoList="toDoList"/>
<Footer/>
</div>
)
}
}
- 接收
props,遍历Item组件;
List/index.jsx:
javascript
import React, {Component} from 'react';
import Item from '../Item/index';
import './index.css'
class List extends Component {
render() {
const {toDoList} = this.props;
return (
<ul className="todo-list">
{
toDoList.map((todo) => {
return <Item {...todo} key={todo.id}/>
})
}
</ul>
);
}
}
export default List;
- 使用
defaultChecked暂时替代checked+onChange;
Item/index.jsx:
javascript
import React, {Component} from 'react';
import './index.css'
class Item extends Component {
render() {
const {name, done} = this.props;
return (
<li>
<input type="checkbox" checked={done}/>
<span>{name}</span>
</li>
);
}
}
export default Item;
我们可以看到这里使用checked,控制台报错了,提示我们checked要和onChange事件一起使用

这里我们换一种或写法,使用defaultChecked属性来实现这里的勾选效果(现在没有问题,之后的交互会导致一些bug,后面会说)
javascript
<li>
<input type="checkbox" defaultChecked={done}/>
<span>{name}</span>
</li>
defaultChecked:默认是否勾选,以后可以修改;checked:是否勾选,但是不可改,想修改必须结合onChange事件一起使用;
2.4 添加一个todo
- 输入框绑定键盘事件,监听回车键;
Header/index.jsx:
javascript
import React, {Component} from 'react';
import './index.css'
class Header extends Component {
render() {
return (
<div className="header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认"
onKeyUp={this.handleKeyUp}/>
</div>
);
}
// 绑定键盘事件
handleKeyUp = (event) => {
// 解构赋值获取keyCode,target
const {target, keyCode} = event;
// 判断是否是回车按键
if (keyCode !== 13) {
return;
}
console.log(target.value);
}
}
export default Header;
- 添加
addTodo函数,实现子组件向父组件传递件数据;
App.jsx
javascript
render() {
const {toDoList} = this.state;
return (
<div className="todo-container">
{/* 将addTodo传递给header */}
<Header addTodo={this.addTodo}/>
<List toDoList={toDoList} />
<Footer/>
</div>
)
}
// addTodo用于添加一个todo,按收的参数是 todo对象
addTodo = (todoObj) => {
// 获取原todos
const {toDoList} = this.state;
//追加一个 todo 对象,并更新状态
this.setState({toDoList: [todoObj, ...toDoList]});
}
此时我们的handleKeyUp函数需要返回新添加的todo对象。
Header/index.jsx:
javascript
// 绑定键盘事件
handleKeyUp = (event) => {
// 解构赋值获取keyCode,target
const {target, keyCode} = event;
// 判断是否是回车按键
if (keyCode !== 13) {
return;
}
// 准备好一个todo对象
const {addTodo} = this.props;
// 将todo对象传递给App
addTodo({
id: '4',
name: target.value,
done: false
});
}
但是新todo对象的id这么直接定义有点问题。
- 使用
nanoid生成唯一id;
我们可以使用npm包uuid和nanoid来生成唯一id,因为nanoid体积更小,所以我们更推荐。
NanoID 是一种轻量级、安全且 URL 友好的唯一字符串 ID 生成器,被视为 UUID 的现代替代品。它通过 Node.js 安装,默认生成 21 个字符的唯一 ID,广泛用于数据库主键、订单号生成 以及前端(如 Vue/React)项目中生成唯一标识。
- 安装:
javascript
npm i nanoid
- 基本使用:
javascript
import { nanoid } from 'nanoid';
const id = nanoid(); // 生成默认长21位的唯一ID
- 指定长度:
javascript
const shortId = nanoid(10); // 生成指定长度的唯一ID
我们使用nanoid来生成新todo对象的id。
javascript
import React, {Component} from 'react';
import {nanoid} from 'nanoid';
import './index.css'
class Header extends Component {
render() {
return (
<div className="header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认"
onKeyUp={this.handleKeyUp}/>
</div>
);
}
// 绑定键盘事件
handleKeyUp = (event) => {
// 解构赋值获取keyCode,target
const {target, keyCode} = event;
// 判断是否是回车按键
if (keyCode !== 13) {
return;
}
// 准备好一个todo对象
const {addTodo} = this.props;
// 将todo对象传递给App
addTodo({
id: nanoid(),
name: target.value,
done: false
});
}
}
export default Header;
- 细节优化(添加的
todo名字不能为空 以及 清空输入);
javascript
// 绑定键盘事件
handleKeyUp = (event) => {
// 解构赋值获取keyCode,target
const {target, keyCode} = event;
// 判断是否是回车按键
if (keyCode !== 13) {
return;
}
// 添加的 todo名字不能为空
if (target.value.trim() === '') {
alert('输入不能为空!')
return;
}
// 准备好一个todo对象
const {addTodo} = this.props;
// 将todo对象传递给App
addTodo({
id: nanoid(),
name: target.value,
done: false
});
// 清空输入
target.value = ''
}
代码写完了,我们添加一个"逛街"的todo,看下页面效果:

2.5 鼠标移入效果(背景色高亮并显示删除按钮)
- 添加鼠标移入和移出事件;
Item/index.jsx:
javascript
import React, {Component} from 'react';
import './index.css'
class Item extends Component {
render() {
const {name, done} = this.props;
return (
<li className="item" onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}>
<input type="checkbox" defaultChecked={done}/>
<span>{name}</span>
<button className="clear-btn right">删除</button>
</li>
);
}
// 鼠标移入移出的回调
handleMouse = (flag) => {
return () => {
console.log(flag);
}
}
}
export default Item;
- 在状态中定义变量
isMouse,作为鼠标移入移出的标识;
javascript
import React, {Component} from 'react';
import './index.css'
class Item extends Component {
// 鼠标移入移出的标识
state = {
isMouse: false
}
render() {
const {name, done} = this.props;
return (
<li className="item" onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}>
<input type="checkbox" defaultChecked={done}/>
<span>{name}</span>
<button className="clear-btn right">删除</button>
</li>
);
}
// 鼠标移入移出的回调
handleMouse = (flag) => {
return () => {
this.setState({isMouse: flag})
}
}
}
export default Item;
- 根据
isMouse标识判断鼠标是否处于移入或移出的状态,并设置样式;
javascript
render() {
const {name, done} = this.props;
const {isMouse} = this.state;
return (
<li className="item" onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}
style={{background: isMouse ? '#eee' : '#fff'}}>
<input type="checkbox" defaultChecked={done}/>
<span>{name}</span>
<button className="clear-btn right" style={{display: isMouse ? 'block' : 'none'}}>删除</button>
</li>
);
}
2.6 勾选或取消勾选某一个todo
- 对勾选框添加
onChange事件;
Item/index.jsx:
javascript
render() {
const {id, name, done} = this.props;
const {isMouse} = this.state;
return (
<li className="item" onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}
style={{background: isMouse ? '#eee' : '#fff'}}>
<input type="checkbox" defaultChecked={done} onChange={this.handleCheck(id)}/>
<span>{name}</span>
<button className="clear-btn right" style={{display: isMouse ? 'block' : 'none'}}>删除</button>
</li>
);
}
// 绑定勾选事件
handleCheck = (id) => {
return (event) => {
console.log(id, event.target.checked)
}
}
- 通知最外层
App更新当前todo对象的勾选框情况;
App.jsx:
javascript
// updateTodo用于更新一个todo对象
updateTodo = (id, done) => {
// 获取状态中的todos
const {toDoList} = this.state;
// 匹配处理数据
const newToDoList = toDoList.map(todoObj => {
if (todoObj.id === id) {
return {...todoObj, done}
} else {
return todoObj;
}
})
this.setState({toDoList: newToDoList});
}
- 由于
App和Item是祖孙关系,所以我们需要一层层传递updateTodo,App先传递给List,List再传递给Item;
App.jsx:
javascript
render() {
const {toDoList} = this.state;
return (
<div className="todo-container">
{/* 将addTodo传递给header */}
<Header addTodo={this.addTodo}/>
<List toDoList={toDoList} updateTodo={this.updateTodo}/>
<Footer/>
</div>
)
}
List/index.jsx:
javascript
import React, {Component} from 'react';
import Item from '../Item/index';
import './index.css'
class List extends Component {
render() {
const {toDoList, updateTodo} = this.props;
return (
<ul className="todo-list">
{
toDoList.map((todo) => {
return <Item {...todo} key={todo.id} updateTodo={updateTodo}/>
})
}
</ul>
);
}
}
export default List;
Item/index.jsx:
javascript
// 绑定勾选事件
handleCheck = (id) => {
return (event) => {
const {updateTodo} = this.props;
updateTodo(id, event.target.checked)
}
}
总结:状态在哪里,操作状态的方法就在哪里。
2.7 对 props 进行限制
- 下载
prop-types库;
javascript
npm i prop-types
- 引入
prop-types;
javascript
import PropTypes from 'prop-types
- 对传递的
props进行类型和必要性的限制;
App.jsx:
javascript
render() {
const {toDoList} = this.state;
return (
<div className="todo-container">
{/* 将addTodo传递给header */}
<Header addTodo={this.addTodo}/>
<List toDoList={toDoList} updateTodo={this.updateTodo}/>
<Footer/>
</div>
)
}
Header/index.jsx:
javascript
import PropTypes from 'prop-types'
...
class Header extends Component {
// 对接收的props进行:类型、必要性的限制
static propTypes = {
addTodo: PropTypes.func.isRequired
}
}
List/index.jsx:
javascript
import PropTypes from 'prop-types'
...
class List extends Component {
// 对接收的props进行:类型、必要性的限制
static propTypes = {
toDoList: PropTypes.array.isRequired,
updateTodo: PropTypes.func.isRequired
}
}
Item/index.jsx:
javascript
import PropTypes from 'prop-types'
...
class List extends Component {
// 对接收的props进行:类型、必要性的限制
static propTypes = {
updateTodo: PropTypes.func.isRequired
}
}
2.8 删除一个 todo
- 添加点击删除按钮的回调函数;
Item/index.jsx:
javascript
render() {
const {id, name, done} = this.props;
const {isMouse} = this.state;
return (
<li className="item" onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}
style={{background: isMouse ? '#eee' : '#fff'}}>
<input type="checkbox" defaultChecked={done} onChange={this.handleCheck(id)}/>
<span>{name}</span>
<button className="clear-btn right" style={{display: isMouse ? 'block' : 'none'}}
onClick={() => this.handleDelete(id)}>删除
</button>
</li>
);
}
...
handleDelete = (id) => {
console.log(id);
}
- 父组件
App要有一个删除todo对象的方法,并传递给Item;
App.jsx:
javascript
render() {
const {toDoList} = this.state;
return (
<div className="todo-container">
{/* 将addTodo传递给header */}
<Header addTodo={this.addTodo}/>
<List toDoList={toDoList} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
<Footer/>
</div>
)
}
...
// deleteTodo用于删除一个todo对象
deleteTodo = (id) => {
// 获取原来的todos
const {toDoList} = this.state;
// 删除指定id的 todo 对象
const newToDoList = toDoList.filter(todoObj => {
return todoObj.id !== id;
})
// 更新状态
this.setState({toDoList: newToDoList});
}
- 先将
deleteTodo传递给List,在通过List再传递给Item,并对props进行限制;
List/index.jsx:
javascript
import React, {Component} from 'react';
import Item from '../Item/index';
import PropTypes from 'prop-types'
import './index.css'
class List extends Component {
// 对接收的props进行:类型、必要性的限制
static propTypes = {
toDoList: PropTypes.array.isRequired,
updateTodo: PropTypes.func.isRequired,
deleteTodo: PropTypes.func.isRequired
}
render() {
const {toDoList, updateTodo, deleteTodo} = this.props;
return (
<ul className="todo-list">
{
toDoList.map((todo) => {
return <Item {...todo} key={todo.id} updateTodo={updateTodo} deleteTodo={deleteTodo}/>
})
}
</ul>
);
}
}
export default List;
注意:delete是一个关键字,用于删除某个对象里的指定属性,比如:
javascript
let obj = {a: 1};
delete obj.a;
所以删除todo对象的函数千万不要命名为delete!
Item/index.jsx:
javascript
// 删除一个todo的回调
handleDelete = (id) => {
const {deleteTodo} = this.props;
deleteTodo(id);
}
- 添加删除提示;
为了防止用户误删,我们需要在删除操作前添加一个确认删除的提示。
javascript
// 删除一个todo的回调
handleDelete = (id) => {
const {deleteTodo} = this.props;
deleteTodo(id);
}
2.9 实现底部功能
- 计算已完成的个数和总数;
先将App状态中的toDoList作为props传给Footer。
App.jsx:
javascript
render() {
const {toDoList} = this.state;
return (
<div className="todo-container">
{/* 将addTodo传递给header */}
<Header addTodo={this.addTodo}/>
<List toDoList={toDoList} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
<Footer toDoList={toDoList}/>
</div>
)
}
对数组进行条件统计,如果done值为true,则进行+1累计;
Footer/index.jsx:
javascript
import React, {Component} from 'react';
import './index.css'
class Footer extends Component {
render() {
const {toDoList} = this.props;
// 总数
const total = toDoList.length;
// 已完成的个数
const doneCount = toDoList.reduce((prev, cur) => prev + (cur.done ? 1 : 0), 0)
return (
<div className="footer-section">
<div className="stats">
<input type="checkbox"/>
<span>已完成{doneCount} / 全部{total}</span>
</div>
<button className="clear-btn">清除已完成任务</button>
</div>
);
}
}
export default Footer;
- 底部全选框☑️
当已完成的个数和总数相等,则勾选全选框;
javascript
import React, {Component} from 'react';
import './index.css'
class Footer extends Component {
render() {
const {toDoList} = this.props;
// 总数
const total = toDoList.length;
// 已完成的个数
const doneCount = toDoList.reduce((prev, cur) => prev + (cur.done ? 1 : 0), 0)
return (
<div className="footer-section">
<div className="stats">
<input type="checkbox" checked={doneCount === total ? true : false}/>
<span>已完成{doneCount} / 全部{total}</span>
</div>
<button className="clear-btn">清除已完成任务</button>
</div>
);
}
}
export default Footer;
效果是没有问题的,但是控制台报错了,提示checked属性要和onChange事件一起使用;

添加全选框的onChange事件;
javascript
render() {
const {toDoList} = this.props;
// 总数
const total = toDoList.length;
// 已完成的个数
const doneCount = toDoList.reduce((prev, cur) => prev + (cur.done ? 1 : 0), 0)
return (
<div className="footer-section">
<div className="stats">
<input type="checkbox" checked={doneCount === total ? true : false} onChange={this.handleCheckAll}/>
<span>已完成{doneCount} / 全部{total}</span>
</div>
<button className="clear-btn">清除已完成任务</button>
</div>
);
}
// 全选chekcbox的回调
handleCheckAll = (event) => {
console.log(event.target.checked);
}
App声明一个全选/全不选的方法,用于Footer通知App改变全部勾选状态;
App.jsx:
javascript
render() {
const {toDoList} = this.state;
return (
<div className="todo-container">
{/* 将addTodo传递给header */}
<Header addTodo={this.addTodo}/>
<List toDoList={toDoList} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
<Footer toDoList={toDoList} checkAllTodos={this.checkAllTodos}/>
</div>
)
}
...
// checkAllTodo用于全选
checkAllTodos = (done) => {
// 获取原来的todos
const {toDoList} = this.state;
// 加工数据
const newToDoList = toDoList.map(todoObj => {
return {...todoObj, done};
})
// 更新状态
this.setState({toDoList: newToDoList});
}
Footer/index.jsx:
javascript
// 全选chekcbox的回调
handleCheckAll = (event) => {
const {checkAllTodos} = this.props;
checkAllTodos(event.target.checked)
}
此时,我们将Item中的checkbox的defaultChecked替换成checked。
javascript
<input type="checkbox" checked={done} onChange={this.handleCheck(id)}/>
我们还要注意一个特殊情况,当保持全部勾选状态,删除todo对象直至为0时,全选框应该取消勾选:


Footer/index.jsx:
javascript
<input type="checkbox" checked={doneCount === total && total !== 0 ? true : false} onChange={this.handleCheckAll}/>
- 清除已完成任务
先在清除按钮上绑定点击事件:
javascript
<button className="clear-btn" onClick={this.handleClearAllDone}>清除已完成任务</button>
...
// 清除已完成任务的回调
handleClearAllDone = () => {
// 通知App.jsx更改状态
}
App声明一个过滤掉已完成todo对象的方法,用于Footer通知App改变数据状态;
App.jsx:
javascript
<Footer toDoList={toDoList} checkAllTodos={this.checkAllTodos} clearAllDone={this.clearAllDone}/>
...
// clearAllDone用于清除所有已完成的
clearAllDone = () => {
// 获取原来的todos
const {toDoList} = this.state;
// 过滤数据
const newToDoList = toDoList.filter(todoObj => {
return !todoObj.done;
})
// 更新状态
this.setState({toDoList: newToDoList});
}
Footer/index.jsx:
javascript
// 清除已完成任务的回调
handleClearAllDone = () => {
this.props.clearAllDone();
}
2.10 总结 TodoList 案例
-
拆分组件、实现静态组件,注意:
className、style的写法; -
动态初始化列表,如何确定将数据放在哪个组件的
state中?
- 某个组件使用 :放在其自身的
state中; - 某些组件使用 :放在他们共同的父组件state 中(官方称此操作为:状态提升;
- 关于父子之间通信:
- 【父组件】给【子组件】传递数据:通过
props传递 - 【子组件】给【父组件】传递数据:通过
props传递,要求父提前给子传递一个函数;
- 注意
defaultchecked和checked的区别,类似的还有:defaultValue和value; - 状态在哪里,操作状态的方法就在哪里!