React 实践之 TodoList | 青训营

127 阅读4分钟

使用 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;

新进入页面时加载了原始的数据

image.png

新增待办事项

image-20230827203845-9qxxyql.png

image.png

编辑待办事项

image.png

image.png

删除待办事项

image.png