React初入门 - 用Ts完成一个Todo

用Ts完成一个Todo

使用React 实现一个简单的待办事项列表:用户可以添加、编辑和删除待办事项;

首先终端运行create-react-app todo --template typescript创建一个react.tsx项目

运行项目npm start,把多余的不要的文件清一下。css和一些静态文件

一、大致确定类型

每一条Todo的类型叫做todo需要包括id,content,completed,id用于区分和key值,content内容,completed表明是否打勾完成。

useReducer作为全局状态的state,要包含一个数组todoList,类型为todo[]

二、编写功能代码

  1. App里搞一个Todo组件,Todo组件下面应该有两个组件,一个负责打字添加一条新的 TodoInput,一个负责展示list数据,每条数据后面要有删除和打勾的功能按钮 TodoList

src/App.tsx 代码

tsx 复制代码
import Todo from './components/Todo';

function App() {
  return (
    <div className="App">
        <Todo />
    </div>
  );
}

export default App;
  1. 做受控组件,把数据列表todoList,添加addTodo,删除removeTodo,打勾checkTodo写在最外层的Todo组件中,通过props进行传递。接下来我们完成TodoInput和TodoList组件和一个todoReducer即可

src/components/Todo/index.tsx 代码

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;
  1. 定义一下我们的类型,因为ts需要声明类型所以先把我们需要的类型定义好,例如,ITodo,IState,IAction。Reducer也独立出来写在todoReducer.tsx

src/components/Todo/typings/index.tsx 代码

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 代码

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;
  }
}
  1. TodoInput和TodoList组件的代码如下,TodoList因为涉及到遍历,展示文本加checkbox加按钮就多弄个组件TodoItem,这几个比较简单难点还是在Reducer中啦,接受props记得声明好类型噢

src/component/Todo/TodoInput/index.tsx代码

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代码

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代码

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代码:19行

1.原先为[ ]修改为从localStorage获取,JSON.parse(localStorage.getItem("todoList")??"[]"),

tsx 复制代码
  const [todoState, todoDispatch] = useReducer(
    todoReducer,
    JSON.parse(localStorage.getItem("todoList")??"[]"), // 从localStorage获取,如果获取不到那就来一个空数组的字符串,让其转为[]而不是"[]"
    todoInit
  );

2.用一个useEffect来同步本地数据,每次state更新我们就保存一次localStorage

添加代码 42行,记得导入useEffect方法

tsx 复制代码
  useEffect(() => {
    localStorage.setItem(
      "todoList",
      JSON.stringify(todoState.todoList) // localStorage只接受字符串所以要转一下
    );
  }, [todoState.todoList]);

完成展示

相关推荐
光影少年8 小时前
usemeno和usecallback区别及使用场景
react.js
吕彬-前端12 小时前
使用vite+react+ts+Ant Design开发后台管理项目(二)
前端·react.js·前端框架
小白小白从不日白12 小时前
react hooks--useCallback
前端·react.js·前端框架
恩婧13 小时前
React项目中使用发布订阅模式
前端·react.js·前端框架·发布订阅模式
程序员小杨v114 小时前
如何使用 React Compiler – 完整指南
前端·react.js
谢尔登15 小时前
Babel
前端·react.js·node.js
卸任15 小时前
使用高阶组件封装路由拦截逻辑
前端·react.js
清汤饺子17 小时前
实践指南之网页转PDF
前端·javascript·react.js
霸气小男18 小时前
react + antDesign封装图片预览组件(支持多张图片)
前端·react.js
小白小白从不日白18 小时前
react 组件通讯
前端·react.js