• 使用React 实现一个简单的待办事项列表
  • 发布于 2个月前
  • 298 热度
    0 评论
一、大致确定类型

每一条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';
// 堆代码 duidaima.com
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 * @ 堆代码 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。


第二是payload包含两种类型,在使用时要记得断言as强调是哪种类型,否则会报奇奇怪怪的错误例如什么 dispatch的 应有 0 个参数,但获得 1 个 [ ] 没有与此调用匹配的重载。最后一个重载给出了以下错误。类型“never[]”的参数不能赋给类型“never”的参数。ts(2769)
src/components/Todo/typings/todoReducer.tsx 代码
/*
 * @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记得声明好类型噢!

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
 * @堆代码 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内容保存到本地
你不会想着刷新页面,今天的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]);



用户评论