本文属于代码分析,而非从0到1的代码书写。
本文会将每一段重要代码的含义都分析清除,然后在代码后面通过设定一些小问题,考验一下大家是否对 “各种语句” 以及 “不同场景所需逻辑” 的使用已经掌握牢固。
Redux
借助TS的Hooks操作数据,可以说是非常的简便了,但是如果想要灵活地使用Hooks,了解React的Redux依旧是重中之

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

根据项目的结构,将项目分为上下两个组件,上方是输入组件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个核心要点:
- 什么是状态惰性初始化?为什么要对初始化的状态做惰性初始化处理?
- 什么时候用useState创建状态?什么时候用useReducer创建状态?
- useEffect依赖为空时,什么时候被触发?依赖非空时,什么时候被触发?
- 什么时候将数据存储到localStorage中?什么时候取出来?
-
dispatch里面传了什么?它又在做什么?
- 我们需要向组件中传递什么?
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个核心
- todoListReducer在什么时候被调用?
- 我们有传递参数和preTodoList相对应吗?
- action是哪里传递来的参数?它里面都有什么?
- 我们根据不同的行为,需要返回什么数据?是返回处理的单条数据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}/>
<button onClick={addItem}>button</button>
</div>
)
}
export default TdInput;
这里面有5个关键点:
- 接口是用来限制谁的?
- 从父组件中传递过来的参数是一个对象?还是一个参数列表?
- 如何用ref来标注一个标签?
- xxx.yyy.zzz,yyy一定有值,可是还是会提示yyy可能为undefined怎么办?
- 从父组件传递过来的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 }
/>
<!-- 我们需要为已经完成的todo添加一个删除线 -->
<span
style={ { textDecoration: isCompleted ? 'line-through' : 'none' } }
>{content}</span>
<!-- 当我们点击删除时,只需要删除掉对应id的todo即可 -->
<button
onClick={ () => removeTodo(id) }
>删除</button>
</div>
)
}
export default TdItem;
这里我们需要明白2个关键点:
- 如何让我们的点击,反馈到方框中?
- 如何让我们的点击,改变todo中的数据?