每一条Todo的类型叫做todo需要包括id,content,completed,id用于区分和key值,content内容,completed表明是否打勾完成。useReducer作为全局状态的state,要包含一个数组todoList,类型为todo[]
import Todo from './components/Todo'; // 堆代码 duidaima.com function App() { return ( <div className="App"> <Todo /> </div> ); } export default App;做受控组件,把数据列表todoList,添加addTodo,删除removeTodo,打勾checkTodo写在最外层的Todo组件中,通过props进行传递。接下来我们完成TodoInput和TodoList组件和一个todoReducer即可。
import { useReducer } from "react"; import { ACTION_TYPE, ITodo } from "./typings"; import { todoReducer } from "./typings/todoRedcuer"; import TodoInput from "./TodoInput"; import TodoList from "./TodoList"; /* * @Date: 2023-08-07 09:58:16 * @Author: WaterRec */ function Todo() { const todoInit = (initTodoList: ITodo[]) => { return { todoList: initTodoList, }; }; const [todoState, todoDispatch] = useReducer( todoReducer, [], todoInit ); function addTodo(todo: ITodo): void { todoDispatch({ type: ACTION_TYPE.ADD_TYPE, payload: todo, }); } function removeTodo(id: number): void { todoDispatch({ type: ACTION_TYPE.REMOVE_TYPE, payload: id, }); } function toggleTodo(id: number): void { todoDispatch({ type: ACTION_TYPE.TOGGLE_TYPE, payload: id, }); } return ( <div className="todo"> <TodoInput todoList={todoState.todoList} addTodo={addTodo} /> <TodoList todoList={todoState.todoList} removeTodo={removeTodo} toggleTodo={toggleTodo} /> </div> ); } export default Todo;定义一下我们的类型,因为ts需要声明类型所以先把我们需要的类型定义好,例如,ITodo,IState,IAction。Reducer也独立出来写在todoReducer.tsx
/* * @Date: 2023-08-07 10:09:22 * @ 堆代码 duidaima.com */ // 指明每一条todo的值的类型 export interface ITodo { id: number; content: string; completed: boolean; } // 全局状态(内部目前只需要一个todoList) export interface IState { todoList: ITodo[]; } // 指明dispatch必须携带的action类型 export interface IAction { type: ACTION_TYPE; payload: number | ITodo; // check和remove只需要知道id, 添加需要一个ITodo } // action的type类型枚举 export enum ACTION_TYPE { ADD_TYPE = "add", REMOVE_TYPE = "remove", TOGGLE_TYPE = "toggle", }
写一下reducer的逻辑叭,action包含三个状态,ADD_TYPE = "add", REMOVE_TYPE = "remove", TOGGLE_TYPE = "toggle", 还好我们在类型声明里面先写好了枚举类型,以后要用直接导入用大写变量就可以了。
这里要注意,尽量使用展开运算符,而不是直接对state中的元素修改,state的元素的元素也不要修改也是用展开运算符,重新创建一个新的对象。强调:避免直接修改state。
/* * @Date: 2023-08-07 10:11:44 * @堆代码 duidaima.com */ import { ACTION_TYPE, IAction, IState, ITodo, } from "."; export function todoReducer( state: IState, action: IAction ) { switch (action.type) { case ACTION_TYPE.ADD_TYPE: return { ...state, todoList: [ ...state.todoList, action.payload as ITodo, ], }; case ACTION_TYPE.REMOVE_TYPE: return { ...state, todoList: state.todoList.filter( (todo) => todo.id !== action.payload ), }; case ACTION_TYPE.TOGGLE_TYPE: return { ...state, todoList: state.todoList.map((todo) => { if (todo.id === action.payload) { return { ...todo, completed: !todo.completed, }; } return todo; }), }; default: return state; } }TodoInput和TodoList组件的代码如下,TodoList因为涉及到遍历,展示文本加checkbox加按钮就多弄个组件TodoItem,这几个比较简单难点还是在Reducer中啦,接受props记得声明好类型噢!
import { useRef } from "react"; import { ITodo } from "../typings"; /* * @Date: 2023-08-07 13:50:52 * @Author: WaterRec */ interface IProp { todoList: ITodo[]; addTodo: (todo: ITodo) => void; } function TodoInput(props: IProp) { const { todoList, addTodo } = props; const inputRef = useRef<HTMLInputElement>(null); function addHandler(): void { // 获取input元素里面的值 const val = inputRef.current?.value.trim(); let isExist = false; // 存在可添加 if (val) { todoList.forEach((todo) => { isExist = todo.content === val ? true : false; }); if (isExist) { alert("请勿重复添加!"); } else { addTodo({ id: new Date().getTime(), // id为时间戳 content: val, completed: false, }); } } } return ( <div className="todo-input"> <input ref={inputRef} type="text" /> <button onClick={addHandler}>添加</button> </div> ); } export default TodoInput;src/component/Todo/TodoList/index.tsx代码
import { ITodo } from "../typings"; import TodoItem from "./TodoItem"; interface IProps { todoList: ITodo[]; removeTodo: (id: number) => void; toggleTodo: (id: number) => void; } function TodoList(props: IProps) { const { todoList, removeTodo, toggleTodo } = props; return ( <div className="todo-list"> {todoList.map((todo) => ( <TodoItem todo={todo} removeTodo={removeTodo} toggleTodo={toggleTodo} key={todo.id} /> ))} </div> ); } export default TodoList;src/component/Todo/TodoList/TodoItem/index.tsx代码
/* * @Date: 2023-08-07 15:07:39 * @堆代码 duidaima.com */ import { ITodo } from "../../typings"; interface IProps { todo: ITodo; removeTodo: (id: number) => void; toggleTodo: (id: number) => void; } function TodoItem(props: IProps) { const { todo, removeTodo, toggleTodo } = props; return ( <div className="todo-item"> <input type="checkbox" checked={todo.completed} onChange={() => { toggleTodo(todo.id); }} /> <span style={{ textDecoration: todo.completed ? "line-through" : "", }} > {todo.content} </span> <button onClick={() => { removeTodo(todo.id); }} > 删除 </button> </div> ); } export default TodoItem;三、把State内容保存到本地
const [todoState, todoDispatch] = useReducer( todoReducer, JSON.parse(localStorage.getItem("todoList")??"[]"), // 从localStorage获取,如果获取不到那就来一个空数组的字符串,让其转为[]而不是"[]" todoInit );2.用一个useEffect来同步本地数据,每次state更新我们就保存一次localStorage
useEffect(() => { localStorage.setItem( "todoList", JSON.stringify(todoState.todoList) // localStorage只接受字符串所以要转一下 ); }, [todoState.todoList]);