使用 React + TS 编写 TodoList,用于新学知识的巩固实践
前置
创建 React 的 Typescript 模板
npx create-react-app my-todolist --template typescript
配置 @ 路径
用 create-react-app 创建的项目中自动隐藏了 webpack 的配置,需要手动暴露出来。
yarn eject
输入命令之后就会出现 config 文件夹,修改里面的配置文件即可。
在 webpack.config.js 中添加 alias 新的别名 @
/* in config/webpack.config.js */
alias: {
// ...
"@": path.resolve(__dirname, '../src') /* 新增 */
},
安装 Node 18(否则无法使用最新版的 craco)
安装 craco
yarn add @craco/craco
安装 react-scripts(否则 craco 运行时报错 Cannot find module 'react-scripts/package.json' )
yarn add react-scripts
运行
yarn start
组件
TodoList
代办事项的表格,是其他代办事项组件的父组件,用于存储和管理代办事项的数据信息
需要实现待办事项的增加、删除、编辑功能。
结构设计
-
头部
- 显示用户的信息
- 提供新增待办事项的入口
-
内容
- 展示各个待办事项
代码实现
定义传入组件的 props 接口
- user:用户名称
- id:用户 id
- dataList:提供给各个 TodoItem 的数据
export interface ITodoList {
user?: string;
id?: number;
dataList: Array<TodoItemDataType>;
}
组件内容
const TodoList: React.FC<ITodoList> = (props) => {
const {user, id} = props;
const [dataList, setDataList] = useState(props.dataList);
const cnt = useRef(dataList.length);
// ...此处省略事件逻辑...
return (
<div className="list">
<div className="list-head">
<div className="list-info">
<span>用户:{user}</span>
<span>id:{id}</span>
</div>
<TodoInput onSubmit={addTodoItem} />
</div>
<div className="list-body">
{
dataList.length ?
dataList.map((item: TodoItemDataType) => (
item.done ?
null :
<TodoItem data={item} key={item.id} onDelete={deleteTodoItem} onEditSubmit={editTodoItem}></TodoItem>
)) : <span>暂时没有任务哦</span>
}
</div>
</div>
);
};
逻辑处理
新增待办事项
应该对旧的 dataList 进行浅拷贝,然后增加某一项,最后整体提交
const addTodoItem = (name: string) => {
console.log(`${name} 被添加了!`);
const newItem:TodoItemDataType = {
id: ++cnt.current,
name,
done: false
}
setDataList([...dataList, newItem]);
}
删除待办事项
应该对旧的 dataList 进行浅拷贝,然后修改某一项的 done 为 true,最后整体提交
const deleteTodoItem = (id: number) => {
const newDataList = [...dataList];
const toDeleteItem = newDataList.find(el => el.id === id);
console.log(toDeleteItem);
if (toDeleteItem) {
toDeleteItem.done = true;
setDataList(newDataList);
}
}
编辑待办事项
应该对旧的 dataList 进行浅拷贝,然后修改某一项的 name,最后整体提交
const editTodoItem = (id: number, newName: string) => {
const newDataList = [...dataList];
const toEditItem = newDataList.find(el => el.id === id);
if (toEditItem) {
console.log(`修改 ${toEditItem.name} 项`);
toEditItem.name = newName;
setDataList(newDataList);
}
}
TodoInput
一开始是打算将这个组件直接在 TodoList 里面写的,然后靠 state 来维护输入的信息,但是这样会导致剩余的子组件跟着重新渲染(父组件中的 state 更新了),因此需要把这个输入组件单独抽离出来,定义好输入输出。
添加代办事项的输入组件
结构设计
- 输入框:用于键入新待办事项的名称
- 按钮:用于触发添加待办事件
代码实现
定义传入 props 的结构
- name(可选): 初次传入的默认名称
- onSubmit:按钮触发的添加事件(父组件传入)
interface ITodoInput {
name?: string;
onSubmit: (name: string) => void;
}
组件内容
const TodoInput: React.FC<ITodoInput> = (props) => {
const {name, onSubmit} = props;
const [inputVal, setInputVal] = useState(name ?? "");
// ...此处省略事件逻辑...
return (
<div className="input-wrap">
<input type="text" className="input-area" placeholder='请输入任务名称'
value={inputVal} onChange={(e)=> setInputVal(e.target.value)}/>
<button className="input-button" onClick={handleBtnClick}>添加</button>
</div>
);
}
逻辑处理
按钮点击事件的函数
- 如果输入的新待办事项名称为空,则不应该触发添加事件
- 触发添加事件后,将输入框中的名称清空
const handleBtnClick = () => {
if (!inputVal) return;
onSubmit(inputVal);
setInputVal("");
}
TodoItem
每一个待办事项的组件,用于展示待办事项的信息,同时提供编辑和删除的交互
结构设计
- 显示待办事件的序号、名称
- 按钮组,用于编辑和删除
代码实现
定义传入的 props 内容
- key:用于绑定列表中项的唯一值,减少重复的渲染
- data:某一个待办事项的数据
- onDelete:按钮触发的删除事件(父组件传入)
- onEditSubmit:按钮触发的编辑事件(父组件传入)
export interface ITodoItem {
key: number;
data: TodoItemDataType;
onDelete: (id: number) => void;
onEditSubmit: (id: number, newName: string) => void;
}
定义待办事项的数据结构
- id:待办事项的 id
- name:待办事项的名称
- done:待办事项的完成情况
export type TodoItemDataType = {
id: number;
name: string;
done: boolean;
}
组件内容
const TodoItem: React.FC<ITodoItem> = (props) => {
const { onDelete, onEditSubmit } = props;
const { id, name, done } = props.data;
const [isEdit, setIsEdit] = useState(false);
const newName = useRef(name);
// ...此处省略事件逻辑...
return (
<div className="item-content">
<span className="item-id">{id}</span>
<span className="item-name">{isEdit ? (<input type="text" defaultValue={name} onChange={handleNameChange}></input>) : name}</span>
<span className="item-btn">
{isEdit ?
<button className="edit-btn" onClick={handleEditSubmit}>确认修改</button> :
<>
<button className="edit-btn" onClick={handleEditClick}>编辑</button>
<button className="del-btn" onClick={handleDelClick}>删除</button>
</>
}
</span>
</div>
);
}
逻辑处理
删除某个待办事项
const handleDelClick = () => {
onDelete(id);
}
编辑某个待办事项
- 监听输入框的值变化(保存在 Ref 中)
- 开始编辑某个待办事项的名称(进入编辑状态,UI 也会改变)
- 提交编辑后的待办事项名称
const handleNameChange = (e: any) => {
newName.current = e.target.value;
}
const handleEditClick = () => {
console.log("点击了编辑按钮");
setIsEdit(true);
}
const handleEditSubmit = () => {
console.log("设置!")
onEditSubmit(id, newName.current);
setIsEdit(false);
}
测试
定义一个产生测试数据的脚本
import { ITodoList } from '@/components/TodoList'
const nameList = ["吃饭", "睡觉", "游戏", "学习", "番剧"];
const genListConfig = (nameList: Array<string>) => {
const newListConfig: ITodoList = {
user: "JavenLu",
id: 233,
dataList: []
}
nameList?.forEach((name, index) => {
newListConfig.dataList.push(
{
name,
id: index + 1,
done: false
}
)
})
return newListConfig;
}
const listConfig = genListConfig(nameList);
export default listConfig;
新进入页面时加载了原始的数据
新增待办事项
编辑待办事项