React + TS 完成 TodoList

本文属于代码分析,而非从0到1的代码书写。

本文会将每一段重要代码的含义都分析清除,然后在代码后面通过设定一些小问题,考验一下大家是否对 “各种语句” 以及 “不同场景所需逻辑” 的使用已经掌握牢固。


   Redux

借助TS的Hooks操作数据,可以说是非常的简便了,但是如果想要灵活地使用Hooks,了解React的Redux依旧是重中之

  • 组件有各种行为,例如:增、删、改、查。
  • 组件不允许直接修改state,只能先创建出对应的action,里面记录着行为的名称和变化的数据,然后借助dispatch交给store。
  • store将改变前的state和action交给reducer,reducer去操作具体的状态,然后将新的state再存储到store中,store将操作完的state再交给组件。

这就是redux的工作流程,或者是工作机制。


   整体结构

根据项目的结构,将项目分为上下两个组件,上方是输入组件Input,下方是展示组件List

  • TodoList
    • Input
    • List

   src目录结构

我们就根据代码的引入顺序,由外至内一步一步地分析关键的文件。

   App.tsx

// App.tsx

import React from 'react';

import TodoList from './components/TodoList';

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

export default App;

结构十分简单,只需要引入TodoList,然后使用这个组件就可以。

   TodoList.tsx

import React, { FC, ReactElement, useCallback, useEffect, useReducer } from "react";
import TdInput from './Input';
import TdList from "./List";
import { todoListReducer } from "./reducer";
import { ACTION_TYPES, ITodoList, ITodo } from "./typings";

function initTL(initTodoList: ITodo[]): ITodoList{
  return {
    todoList: initTodoList
  }
}
// 状态惰性初始化,使用该方式,会等到useRuducer执行之后才去创建初始化的数据


const TodoList:FC = ():ReactElement=>{ 

  const [state, dispatch] = useReducer(todoListReducer, [], initTL);  
  // 这里我们不使用useState去创建state,一方面是因为:对该数据的大量操作都位于子组件中,而非供自己使用
  // 另一方面:是因为todoList中的数据涉及到增、删、改,对数据的操作相对复杂,用useState无法对数据的深度修改进行优化


  useEffect(() => {
    const todoList = JSON.parse(localStorage.getItem('todolist') || '[]');

    dispatch({
      type: ACTION_TYPES.UPDATE_TODOLIST,
      data: todoList
    })
    // dispatch中有我们的行为,以及对应处理的数据
  }, [])
  // 这个useEffect由于不对任何数据进行检测,所以只会在页面刚加载时执行一次。这个时候,我们需要从本地获取数据


  useEffect(() => {
    localStorage.setItem('todolist', JSON.stringify(state.todoList));
  }, [state.todoList])
  // 当todoList发生变化时,向localStorage中保存最新的数据


  const addTodo = useCallback((todo: ITodo)=>{
    dispatch({
      type: ACTION_TYPES.ADD_TODO,  // 行为
      data: todo  // 修改的数据
    })
    // 当子组件触发addTodo,需要向todoList中添加数据时,只需要告诉dispatch行为是添加,并把添加的数据放进去即可
  }, [])
  // 一个方法,如果使用者不是自己,而是供子组件使用,最好是用useCallback将其包裹起来【第二个参数为依赖】


  const toggleTodo = useCallback((id: number)=>{
    dispatch({
      type: ACTION_TYPES.TOGGLE_TODO,
      data: id
    })
  }, [])

  const removeTodo = useCallback((id: number)=>{
    dispatch({
      type: ACTION_TYPES.REMOVE_TODO,
      data: id
    })
  }, [])


  return (
    <div className="todolist">
      <h1>TodoList</h1>

      <!-- 将操作数据的方法和数据源交给子元素,由子元素决定在什么时候去添加元素 -->
      <TdInput 
        addTodo={addTodo}  
        todoList={state.todoList}
      /> 

      <br />

      <!-- 在List中,包含了切换每一个item完成状态、删除item的操作,因此我们需要将两个方法和数据源都传递过去 -->
      <TdList
        todoList={state.todoList}
        toggleTodo={toggleTodo}
        removeTodo={removeTodo}
      />
    </div>
  );
}

export default TodoList;

在这个组件中,我们需要弄明白6个核心要点:

  1. 什么是状态惰性初始化?为什么要对初始化的状态做惰性初始化处理?
  2. 什么时候用useState创建状态?什么时候用useReducer创建状态?
  3. useEffect依赖为空时,什么时候被触发?依赖非空时,什么时候被触发?
  4. 什么时候将数据存储到localStorage中?什么时候取出来?
  5. dispatch里面传了什么?它又在做什么?
  6. 我们需要向组件中传递什么?

   reducer.ts

import { ACTION_TYPES, IAction, ITodoList, ITodo } from "./typings";

function todoListReducer(preTodoList: ITodoList, action: IAction): ITodoList{
// 在dispatch中包装的action,其实就是传递到了这里,所以这里的action包含了两个属性:type、data
// 这个preTodoList,是不需要我们主动传递一个实参和该形参对应的,因为实际并不是我们在调用的这个reducer,在reducer被调用时,会自动传入一个原始的state,作为变化前的初始状态


  const {type, data} = action; // 取出行为的类型、修改的数据

  // 根据不同的行为,对数据做不同的更改
  switch(type){
    case ACTION_TYPES.ADD_TODO:
      return  {
        todoList: [...preTodoList.todoList, data as ITodo ]  // 返回处理后的数据(原数据+新增的数据)
      }
    case ACTION_TYPES.REMOVE_TODO:
      return {
        todoList: preTodoList.todoList.filter(todo => todo.id !== data)  // 过滤出与被删除的item不同id的项
      }
    case ACTION_TYPES.TOGGLE_TODO:
      return {
        todoList: preTodoList.todoList.map(todo => {  // 将被点击的项的isCompleted取反,其他的项直接返回
          return todo.id === data ?
          {
            ...todo,
            isCompleted: !todo.isCompleted
          }:{
            ...todo
          }
        })
      }
    case ACTION_TYPES.UPDATE_TODOLIST:
      return {
        todoList: data as ITodo[]  // 此时是页面初始化,只需要将从localStorage中取出的数据装入到store中即可
      }
    default:
      return preTodoList;
  }
}

export {
  todoListReducer
}

这里需要明白4个核心

  1. todoListReducer在什么时候被调用?
  2. 我们有传递参数和preTodoList相对应吗?
  3. action是哪里传递来的参数?它里面都有什么?
  4. 我们根据不同的行为,需要返回什么数据?是返回处理的单条数据todo,还是返回完整的数据TodoList?

   TdInput.tsx

import React, {useRef, FC, ReactElement} from "react";
import { ITodo } from "../typings";

// 接口:用来控制参数的种类和个数
interface IInputProps {
  addTodo: (todo: ITodo) => void,
  todoList: ITodo[]
};

const TdInput: FC<IInputProps> = ({
  addTodo,
  todoList
  // 结构赋值,从父组件中传递来的参数其实都存储在一个对象中
}): ReactElement =>{ 

  const inputRef = useRef<HTMLInputElement>(null);
  // ref可以用来标注标签

  const addItem = (): void=>{
    const val: string = inputRef.current!.value.trim();
    // "!" 为我们增加的断言,表示这里一定可以拿到数据
    const isExist = todoList.find(todo => todo.content === val);
    if(isExist){
      alert("该项已存在!");
      return;
    }

    addTodo({
      id: new Date().getTime(),
      content: val,
      isCompleted: false
    })
    // 在我们判定需要调用添加数据后,就可以调用addTodo了,如何传参需要参考父组件,因为这个函数来自父组件,所以只有看了父组件是如何定义的,才能够知道如何使用。
    // 因为这个函数的行为是确定的,就是添加元素,所以我们就只需要在这里定义一个元素,然后作为函数的参数去调用函数就可以了。函数在定义时,也是这么设定的,只需要传递一个参数

    inputRef.current!.value = '';
    // 传递完参数,一定不能忘了将输入框置空
  }

  return (
    <div className="todo-input">
      <input type="text" ref={inputRef}/> &nbsp;
      <button onClick={addItem}>button</button>
    </div>
  )
}

export default TdInput; 

这里面有5个关键点:

  1. 接口是用来限制谁的?
  2. 从父组件中传递过来的参数是一个对象?还是一个参数列表?
  3. 如何用ref来标注一个标签?
  4. xxx.yyy.zzz,yyy一定有值,可是还是会提示yyy可能为undefined怎么办?
  5. 从父组件传递过来的addTodo,我们需要向这个函数传递什么参数?

   TdList.tsx

import React, { FC } from "react";
import { ITodo } from "../typings";
import TdItem from "./Item";

interface IListProps{
  todoList: ITodo[],
  toggleTodo: (id: number) => void
  removeTodo: (id: number) => void,
}

const TdList: FC<IListProps> = ({
  todoList,
  toggleTodo,
  removeTodo
}) => {
  return (
    <div className="todo-list">
      {
        // ↓ 表示todoList 存在的话
        todoList && todoList.map((todo: ITodo)=>{
          return (
            <TdItem 
              key={ todo.id }
              todo = {todo}
              toggleTodo={ toggleTodo }
              removeTodo={ removeTodo }
            />
          )
        })
      }
    </div>
  )
}

export default TdList;

这里就十分简单了,因为我们将List又分为了一个个小的Item组件,所以List组件并没有做太多事情。只是将数据进行简单的遍历,拿到一个个的数据todo,然后将这些数据再传递给TdItem组件,由该组件将一个个的数据创建成一个个Item组件。

但是这里我们一定不能忘了将TodoList组件传递来的toggleTodo、removeTodo方法传递给子组件,因为我们现在是在子组件上去调整对应的完成、删除状态的

   TdList.tsx

import React, { FC, ReactElement } from "react";
import { ITodo } from "../typings";

interface IItemProps{
  todo: ITodo,
  toggleTodo: (id: number) => void
  removeTodo: (id: number) => void
}

const TdItem:FC<IItemProps> = ({
  todo,
  toggleTodo,
  removeTodo
}):ReactElement => {

  const {id, content, isCompleted} = todo

  return (
    <div className="todo-item">
      <!--  
          在点击方框时,需要改变todo的isComplete属性
          当isComplete为true时,就表示已完成,对应checked自然就是true,这个时候方框就是一个被勾选的状态
      -->
      <input 
        type="checkbox" 
        onChange={ ()=>toggleTodo(id) }
        checked={ isCompleted }
      /> &nbsp;

      <!-- 我们需要为已经完成的todo添加一个删除线 -->
      <span 
        style={ { textDecoration: isCompleted ? 'line-through' : 'none' } }
      >{content}</span> &nbsp;

      <!-- 当我们点击删除时,只需要删除掉对应id的todo即可 -->
      <button
        onClick={ () => removeTodo(id) }
      >删除</button>
    </div>
  )
}

export default TdItem;

这里我们需要明白2个关键点:

  1. 如何让我们的点击,反馈到方框中?
  2. 如何让我们的点击,改变todo中的数据?

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

麦田里的POLO桔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值