使用React 实现一个简单的待办事项列表 | 青训营
一、大致确定类型
每一条Todo的类型叫做todo
需要包括id,content,completed,id用于区分和key值,content内容,completed表明是否打勾完成。
useReducer作为全局状态的state
,要包含一个数组todoList
,类型为todo[]
二、编写功能代码
- App里搞一个Todo组件,Todo组件下面应该有两个组件,一个负责打字添加一条新的
TodoInput
,一个负责展示list数据,每条数据后面要有删除和打勾的功能按钮TodoList
src/App.tsx
代码:
import Todo from './components/Todo';
function App() {
return (
<div className="App">
<Todo />
</div>
);
}
export default App;
- 做受控组件,把数据列表todoList,添加addTodo,删除removeTodo,打勾checkTodo写在最外层的Todo组件中,通过props进行传递。接下来我们完成TodoInput和TodoList组件和一个todoReducer即可
src/components/Todo/index.tsx
代码
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
src/components/Todo/typings/index.tsx
代码
/* * @Date: 2023-08-07 10:09:22 * @Author: WaterRec */
// 指明每一条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
第二是payload包含两种类型,在使用时要记得断言as强调是哪种类型,否则会报奇奇怪怪的错误例如什么 dispatch的 应有 0 个参数,但获得 1 个
[ ] 没有与此调用匹配的重载。最后一个重载给出了以下错误。类型“never[]”的参数不能赋给类型“never”的参数。ts(2769)
src/components/Todo/typings/todoReducer.tsx
代码
/*
* @Date: 2023-08-07 10:11:44
* @Author: WaterRec
*/
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记得声明好类型噢
src/component/Todo/TodoInput/index.tsx
代码
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
* @Author: WaterRec
*/
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内容保存到本地
你不会想着刷新页面,今天的todo任务就全部“完成”吧,现在刷新页面所有的记录都会消失。我们使用loaclStorage来保存到本地。找到我们初始化state的地方,
src/component/Todo/TodoInput/index.tsx
中修改src/component/Todo/TodoInput/index.tsx
代码
1.原先为[ ]修改为从localStorage获取,JSON.parse(localStorage.getItem("todoList")??"[]"),
const [todoState, todoDispatch] = useReducer(
todoReducer,
JSON.parse(localStorage.getItem("todoList")??"[]"), // 从localStorage获取,如果获取不到那就来一个空数组的字符串,让其转为[]而不是"[]"
todoInit
);
2.用一个useEffect来同步本地数据,每次state更新我们就保存一次localStorage
添加代码,记得导入useEffect方法
useEffect(() => {
localStorage.setItem(
"todoList",
JSON.stringify(todoState.todoList) // localStorage只接受字符串所以要转一下
);
}, [todoState.todoList]);