介绍
这里将结合之前所学内容,制作一个 To-Do List。
开始
需求分析
在做项目之前,我们需要思考一下,我们这个项目需要实现的功能:
- 添加一个 To-Do
- 删除一个 To-Do
- 在 To-Do 之前添加复选框,显示已完成
- 对所有 To-Do 进行统计
创建项目
使用脚手架快速创建一个项目:
1
| create-react-app todo-list
|

创建好之后,我们可以运行康康效果:

项目架构
创建完项目后,我们可以康康项目架构。
下面对整个项目的文件和目录做了一些解释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| todo-list ├─.gitignore git忽略文件 ├─package.json 项目信息 ├─README.md 项目介绍 ├─src 源码文件 | ├─App.css App 组件样式 | ├─App.js App组件 | ├─App.test.js App组件测试 | ├─index.css 全局样式 | ├─index.js 入口文件 | ├─logo.svg LOGO文件 | ├─reportWebVitals.js 页面性能分析文件 | └setupTests.js 组件单元测试文件 ├─public 静态资源文件 | ├─favicon.ico 网站页面标签图标 | ├─index.html 主页面 | ├─logo192.png LOGO | ├─logo512.png LOGO | ├─manifest.json 应用配置 | └robots.txt 爬虫协议文件
|
安装 UI 框架
这里我们选用 Ant Design 作为这个项目的 UI 框架。
安装 antd:
安装好之后,我们需要在 App.css 第一行插入:
1
| @import "~antd/dist/antd.css";
|
现在,Ant Design 已经安装完成,我们来测试一下 Ant Design 是否安装成功。
在 App.js 引入 antd:
src/App.js1 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
| import logo from './logo.svg'; import { Button } from 'antd'; import './App.css';
function App() { return (
<div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> <Button type="primary">Button</Button> </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> </header> </div> ); }
export default App;
|
接下来打开浏览器,如果在网页中间能够看到一个完整的按钮,那么 antd 就安装成功了:

完成基本布局
对于我们最终需要实现的效果,需要一个页头、添加 TODO、TODO 列表、TODO 统计 四个部分。
我们先把原有的 App.css 的内容清空,只保留引入 antd 的一行:
1
| @import "~antd/dist/antd.css";
|
接下来,我们修改 App.css 添加新的页面样式:
App.css1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @import "~antd/dist/antd.css";
.site-layout-content { min-height: 280px; padding: 24px; background: #fff; }
.logo { float: left; width: 120px; height: 31px; color: #fff; font-size: 20px; }
|
同时修改 App.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { Layout } from "antd"; import "./App.css";
const { Header, Content } = Layout;
function App() { return ( <Layout className="layout"> <Header> <div className="logo">TO-DO</div> </Header> <Content style={{ padding: "0 50px" }}> <div className="site-layout-content"> <div>添加TODO</div> <div>TODO列表</div> <div>TODO统计</div> </div> </Content> </Layout> ); }
export default App;
|
添加完成后,界面如图所示:

数据管理
介绍
按照目前所学的知识,TODO 列表中的数据是全局性的,所以组件的数据,我们需要放在 App 组件里面。
这里呢,我们使用 Hooks 来作存储数据。
TODO 的结构
现在我们来康康每一个 TODO 的结构,根据需求,每个 TODO 应该有:
1 2 3 4
| { title: String, done: Boolean }
|
建立 TODO 数据存储
我们现在可以建立 TODO 的数据存储了。
先创建一个状态:
1
| let [list, setList] = useState([]);
|
接下来创建一个增加项目的函数:
1 2 3 4
| const addItem = (title) => { const newList = [...list, { title, done: false }]; setList(newList); };
|
创建一个更新项目的函数:(使用 map)
1 2 3 4 5 6
| const updateItem = (index, done) => { const newList = list.map((x, i) => i !== index ? x : Object.assign({}, x, { done }) ); setList(newList); };
|
创建一个删除项目的函数:(使用 filter)
1 2 3 4
| const deleteItem = (index) => { const newList = list.filter((x, i) => i !== index); setList(newList); };
|
目前的完整的 App.js 如下:
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 43
| import { useState } from "react"; import { Layout } from "antd"; import "./App.css";
const { Header, Content } = Layout;
function App() { let [list, setList] = useState([]);
const addItem = (title) => { const newList = [...list, { title, done: false }]; setList(newList); };
const updateItem = (index, done) => { const newList = list.map((x, i) => i !== index ? x : Object.assign({}, x, { done }) ); setList(newList); };
const deleteItem = (index) => { const newList = list.filter((x, i) => i !== index); setList(newList); };
return ( <Layout className="layout"> <Header> <div className="logo">TO-DO</div> </Header> <Content style={{ padding: "0 50px" }}> <div className="site-layout-content"> <div>添加TODO</div> <div>TODO列表</div> <div>TODO统计</div> </div> </Content> </Layout> ); }
export default App;
|
TODO 增删改
修改 TODO
接下来,我们创建一个 TODO-ITEM的组件。
我们先在 src 目录创建一个 components 文件夹。
再在 components 文件夹中创建一个 ToDoItem 文件夹,在文件夹中创建 ToDoItem.js 和 index.js。
建好的目录结构如图所示:

我们在 index.js 导出组件模块:
1
| export * from "./ToDoItem";
|
接下来在 ToDoItem.js添加组件,这里使用了 List 组件:
ToDoItem.js1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { List, Checkbox, Button } from "antd";
export function ToDoItem(props) { return ( <List.Item> <Checkbox checked={props.data.done} onChange={() => props.onUpdate()}> {props.data.title} </Checkbox> <Button onClick={() => props.onDelete()} danger> 删除 </Button> </List.Item> ); }
|
创建组件完成后,我们需要在 App.js 引用它。为了方便,我们暂时添加了一条测试数据:
src/App.js1 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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| import { useState } from "react"; import { Layout, List } from "antd"; import "./App.css"; import { ToDoItem } from "./components/ToDoItem";
const { Header, Content } = Layout;
function App() { let [list, setList] = useState([{ title: "测试 0", done: false }]);
const addItem = (title) => { const newList = [...list, { title, done: false }]; setList(newList); };
const updateItem = (index) => { console.log(index); const newList = list.map((x, i) => i !== index ? x : { ...x, done: !x.done } ); setList(newList); };
const deleteItem = (index) => { const newList = list.filter((x, i) => i !== index); setList(newList); };
return ( <Layout className="layout">
<Header> <div className="logo">TO-DO</div> </Header> <Content style=\{\{ padding: "0 50px" \}\}> <div className="site-layout-content"> <div>添加 TODO</div> <div> <List dataSource={list} renderItem={(item, i) => ( <ToDoItem data={item} onUpdate={() => updateItem(i)} onDelete={() => deleteItem(i)} /> )} ></List> </div> <div>TODO 统计</div> </div> </Content> </Layout> ); }
export default App;
|
这里使用了 List 组件来渲染每一条记录。
修改好后,我们可以在浏览器上测试效果:

尝试点击复选框、删除按钮,康康效果。
添加 TODO
接下来,我们通过 Input组件来添加 ToDo。要求按 Enter 来添加。
这里,我们使用受控组件来操作表单。
src/App.js1 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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| import { useState } from "react"; import { Layout, List, Input } from "antd"; import "./App.css"; import { ToDoItem } from "./components/ToDoItem";
const { Header, Content } = Layout;
function App() { let [list, setList] = useState([{ title: "测试 0", done: false }]); let [value, setValue] = useState([]);
const addItem = (title) => { const newList = [...list, { title, done: false }]; setList(newList); };
const updateItem = (index) => { console.log(index); const newList = list.map((x, i) => i !== index ? x : { ...x, done: !x.done } ); setList(newList); };
const deleteItem = (index) => { const newList = list.filter((x, i) => i !== index); setList(newList); };
return ( <Layout className="layout">
<Header> <div className="logo">TO-DO</div> </Header> <Content style=\{\{ padding: "0 50px" \}\}> <div className="site-layout-content"> <div> <Input placeholder="输入要做的事,按 Enter 添加" /> </div> <div> <List dataSource={list} renderItem={(item, i) => ( <ToDoItem data={item} onUpdate={() => updateItem(i)} onDelete={() => deleteItem(i)} /> )} ></List> </div> <div>TODO 统计</div> </div> </Content> </Layout> ); }
export default App;
|
我们完善一下事件:
1 2 3 4 5
| const handleKeyUp = (e) => { if (e.keyCode !== 13) return; addItem(e.target.value); e.target.value = ""; };
|
完善后, App.js 如下:
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| import { useState } from "react"; import { Layout, List, Input } from "antd"; import "./App.css"; import { ToDoItem } from "./components/ToDoItem";
const { Header, Content } = Layout;
function App() { let [list, setList] = useState([]); let [value, setValue] = useState([]); const addItem = (title) => { const newList = [...list, { title, done: false }]; setList(newList); };
const updateItem = (index) => { console.log(index); const newList = list.map((x, i) => i !== index ? x : { ...x, done: !x.done } ); setList(newList); };
const deleteItem = (index) => { const newList = list.filter((x, i) => i !== index); setList(newList); };
const handleKeyUp = (e) => { if (e.keyCode !== 13) return; addItem(e.target.value); };
return ( <Layout className="layout"> <Header> <div className="logo">TO-DO</div> </Header> <Content style={{ padding: "0 50px" }}> <div className="site-layout-content"> <div> <Input onChange={(e) => setValue(e.target.value)} value={value} onKeyUp={handleKeyUp} placeholder="输入要做的事,按 Enter 添加" /> </div> <div> <List dataSource={list} renderItem={(item, i) => ( <ToDoItem data={item} onUpdate={() => updateItem(i)} onDelete={() => deleteItem(i)} /> )} ></List> </div> <div>TODO统计</div> </div> </Content> </Layout> ); }
export default App;
|
实现添加功能后,可以在浏览器测试一下。输入一些 TODO,按下 Enter 查看效果。

统计功能
统计功能就比较简单了。只需要使用 filter 函数对 list 进行过滤,并计数就行。
统计已完成:
1
| const doneCount = list.filter((x) => x.done).length;
|
1
| <div>已完成:{doneCount};总计:{list.length}</div>
|
完整的 App.js:
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| import { useState } from "react"; import { Layout, List, Input } from "antd"; import "./App.css"; import { ToDoItem } from "./components/ToDoItem";
const { Header, Content } = Layout;
function App() { let [list, setList] = useState([]); let [value, setValue] = useState([]);
const addItem = (title) => { const newList = [...list, { title, done: false }]; setList(newList); };
const updateItem = (index) => { console.log(index); const newList = list.map((x, i) => i !== index ? x : { ...x, done: !x.done } ); setList(newList); };
const deleteItem = (index) => { const newList = list.filter((x, i) => i !== index); setList(newList); };
const handleKeyUp = (e) => { if (e.keyCode !== 13) return; addItem(e.target.value); };
const doneCount = list.filter((x) => x.done).length;
return ( <Layout className="layout"> <Header> <div className="logo">TO-DO</div> </Header> <Content style={{ padding: "0 50px" }}> <div className="site-layout-content"> <div> <Input onChange={(e) => setValue(e.target.value)} value={value} onKeyUp={handleKeyUp} placeholder="输入要做的事,按 Enter 添加" /> </div> <div> <List dataSource={list} renderItem={(item, i) => ( <ToDoItem data={item} onUpdate={() => updateItem(i)} onDelete={() => deleteItem(i)} /> )} ></List> </div> <div> 已完成:{doneCount};总计:{list.length} </div> </div> </Content> </Layout> ); }
export default App;
|
完整代码
https://gitee.com/pikoyo/react-examples/tree/master/examples/todo-list