todo list是不少人学习框架的入门经典案例,正好青训营的实践选题有这个方向,就随手写了一个,就当复习练手了。
主要功能:添加、修改、删除、批量删除、搜索;
为了使按钮看起来更美观,参考arco design 的样式指南调整按钮默认颜色、鼠标悬浮颜色和点击颜色;
1. 效果一览
初始化
添加任务
搜索
修改
- 修改内容:双击任务或点击编辑图标
- 修改完成状态
删除
使用prompt
做二次确认
- 删除单个任务
- 批量删除
2. 组件拆分及通信
- 首先是把整体拆分成
TodoHeader
、TodoBody
和TodoFooter
三个组件,由Todolist
父组件传递属性和上下文; - 增删改通过
useReducer
将具体逻辑分理成单独的文件,并创建DispatchContext
进行分发,由子组件自行注入;查通过SearchKeywordContext
进行分发;
模块划分如下:
开始写
由于笔记活动代码文字比例不能太高,后面尽量粘贴图片
首先是类型,这里把操作单独枚举出来,便于在distapch
时选择合适的操作,由参数提示:
/**
* 操作类型枚举
*/
export enum Behavior {
ADD = "添加",
DELETE = "删除",
UPDATE = "修改",
}
/**
* 一个todo的类型
*/
export interface ITodoItem {
id: string;
content: string;
selected: boolean;
edit: boolean;
done: boolean;
}
export interface ITodoState {
todos: ITodoItem[];
}
/**
* reducer的action
*/
export interface IAction {
type: Behavior;
items: ITodoItem[];
}
然后定义reducer
,reducer
函数返回要把state
修改成的值:
- 添加时直接加到数组最后;
- 删除时会收到一个数组,遍历原
state
,对于其中每个元素,如果id在要删除的数组中,就过滤掉; - 修改时遍历参数中的数组,找到id和传入item的id相同的任务,进行替换;
import { Behavior, IAction, ITodoState } from "../../types";
export function reducer(state: ITodoState, action: IAction): ITodoState {
switch (action.type) {
case Behavior.ADD:
return { todos: [...state.todos, ...action.items] };
case Behavior.DELETE:
const delIds = action.items.map((i) => i.id);
return { todos: state.todos.filter((todo) => !delIds.includes(todo.id)) };
case Behavior.UPDATE:
return {
todos: state.todos.map((todo) => action.items.find((item) => todo.id === item.id) ?? todo),
};
default:
throw new Error("should not arrived");
}
}
之后是context
:
DispatchContext
用于传递查任务的state
以及增删改任务的dispatch
,在index.tsx
中提供,在其子组件中注入使用;SearchKeyContext
用于传递搜索的keyword
和setKeyword
方法,在TodoHeader
中修改,在TodoBody
中对任务数组进行过滤;
export interface DispatchContextValue {
state: { todos: ITodoItem[] };
dispatch: Dispatch<IAction>;
}
export const DispatchContext = createContext<DispatchContextValue>({} as DispatchContextValue);
export interface SearchKeyword {
keyword: string;
setKeyword(keyword: string): void;
}
export const SearchKeywordContext = createContext<SearchKeyword>({} as SearchKeyword);
index.tsx
中:
const initData: ITodoState = {
todos: [
{
id: "0",
content: "吃饭",
selected: true,
edit: false,
done: false,
},
{
id: "1",
content: "睡觉",
selected: false,
edit: false,
done: true,
},
{
id: "2",
content: "学习",
selected: true,
edit: false,
done: false,
},
],
};
export default function TodoList() {
const [state, dispatch] = useReducer<Reducer<ITodoState, IAction>>(reducer, initData);
const [keyword, setKeyword] = useState<string>("");
return (
<DispatchContext.Provider value={{ state, dispatch }}>
<SearchKeywordContext.Provider value={{ keyword, setKeyword }}>
<TodoHeader></TodoHeader>
<TodoBody></TodoBody>
<TodoFooter></TodoFooter>
</SearchKeywordContext.Provider>
</DispatchContext.Provider>
);
}
之后开始写三个子组件:
TodoHeader
作用:搜索和添加
- 修改搜索输入框时会调用context中的
setKeyword
方法,同时后面的TodoBody
中会根据keyword
对展示列表进行过滤: - 添加时可以直接通
dispatch
修改state
,TodoBody
中也会同步更新; - 通过绝对定位在搜索框中添加一个
x
,便于快速清除输入,而且只在输入.trim()
不为空字符串时才显示;
TodoBody
作用:显示、过滤、修改、删除、选择;
- 修改、删除时会调用从context获取的
dispatch
方法; - 通过把编辑
input
的ref
设置为一个函数,可以在不使用useRef
的情况下,获取到DOM
之后直接获取焦点,即切换为编辑模式后自动获取焦点;
TodoFooter
作用:全(不)选、批量删除;
- 也是通过
distapch
进行修改和删除; - 全(不)选切换时使用数组的
every
方法,如果所有的都done
说明当前已经全选,都改为未选中;否则说明至少有一个未选择,改为都选中;
总结
通过一个简单的todo list案例,实现了任务的增删改查。todo list是入门框架的经典案例,可以作为练习组件拆分、组件通信等基础技能的敲门砖,熟练掌握其中包含的原理对日后的开发有一定帮助。