store.ts - 类型定义 + 初始状态
javascript
import { nanoid } from 'nanoid'
// 定义单个 Todo 的类型(约束结构:id+标题)
export type TodoType = {
id: string
title: string
}
// 初始状态:一个包含2个Todo的数组,用nanoid生成唯一id
const initialState: TodoType[] = [
{ id: nanoid(5), title: '吃饭' },
{ id: nanoid(5), title: '睡觉' },
]
export default initialState
作用:
• 用 TypeScript 约束 Todo 数据结构,避免类型错误;
• 定义初始的 Todo 列表,作为 useReducer 的初始值;
• nanoid:生成短且唯一的 ID(替代 UUID,更轻量)。
reducer.ts - 状态更新逻辑(核心)
javascript
import type { TodoType } from './store'
// 定义动作类型(type:动作标识,payload:附加数据)
export type ActionType = {
type: string
payload?: any // 可传:新增的Todo对象 / 要删除的Todo ID
}
// reducer是纯函数:接收「当前状态」和「动作」,返回「新状态」
export default function reducer(state: TodoType[], action: ActionType) {
switch (action.type) {
// 新增Todo:用concat返回新数组(不可变,不修改原state)
case 'add':
return state.concat(action.payload)
// 删除Todo:用filter过滤掉目标ID,返回新数组
case 'delete':
return state.filter(todo => todo.id !== action.payload)
// 未知动作抛错,避免逻辑遗漏
default:
throw new Error()
}
}
核心原则:
- 纯函数:无副作用、输入相同则输出相同;
- 不可变数据:绝不直接修改
state(如state.push()是错误的),而是返回新数组(concat/filter),保证 React 能检测到状态变化并重新渲染。
index.tsx - 根组件 + Context 提供
javascript
import React, { FC, createContext, useReducer } from 'react'
import reducer, { ActionType } from './reducer'
import initialState from './store'
import List from './List'
import InputForm from './InputForm'
// 1. 创建Context:定义上下文的类型(包含state和dispatch),并设置默认值(空实现)
export const TodoContext = createContext({
state: initialState,
dispatch: (action: ActionType) => {},
})
const Demo: FC = () => {
// 2. 初始化useReducer:接收reducer和初始状态,返回[当前状态, 派发动作的函数]
const [state, dispatch] = useReducer(reducer, initialState)
return (
// 3. 用Context.Provider包裹子组件,把state和dispatch注入上下文
<TodoContext.Provider value={{ state, dispatch }}>
<p>Todo list by useReducer</p>
<List /> {/* 子组件:展示Todo */}
<InputForm /> {/* 子组件:新增Todo */}
</TodoContext.Provider>
)
}
export default Demo
关键作用:
• createContext:创建跨组件通信的上下文,让子组件无需通过 props 传递状态;
• useReducer:替代 useState 管理复杂状态(多动作类型:新增 / 删除);
• Provider:把 state(当前 Todo 列表)和 dispatch(触发状态更新的函数)暴露给所有子组件。
InputForm.tsx - 新增 Todo 组件
javascript
import React, { FC, ChangeEvent, useState, useContext } from 'react'
import { nanoid } from 'nanoid'
import { TodoContext } from './index'
const InputForm: FC = () => {
// 从Context中获取全局的state和dispatch
const { state, dispatch } = useContext(TodoContext)
// 局部状态:管理输入框的文字(无需全局共享,所以用useState)
const [text, setText] = useState('')
// 输入框变化时:更新局部状态text
function handleChange(event: ChangeEvent<HTMLInputElement>) {
setText(event.target.value)
}
// 提交表单时:新增Todo
function handleSubmit(event: ChangeEvent<HTMLFormElement>) {
event.preventDefault() // 阻止表单默认提交行为
if (!text.trim()) return // 空内容不处理
// 构建新Todo对象
const newTodo = {
id: nanoid(5),
title: text,
}
// 派发「add」动作:传递新Todo作为payload
dispatch({ type: 'add', payload: newTodo })
setText('') // 清空输入框
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="new-todo">What needs to be done?</label>
<br />
<input id="new-todo" onChange={handleChange} value={text} />
{/* 按钮文字:展示下一个Todo的序号(state.length+1) */}
<button type="submit">Add #{state.length + 1}</button>
</form>
)
}
export default InputForm
核心逻辑:
• 局部状态 text:管理输入框内容(仅组件内使用,无需全局);
• handleSubmit:校验内容 → 构建新 Todo → 调用 dispatch 派发 add 动作 → 清空输入;
• useContext(TodoContext):从上下文获取全局的 dispatch,触发状态更新。
List.tsx - 展示 / 删除 Todo 组件
javascript
import React, { FC, useContext } from 'react'
import { TodoContext } from './index'
const List: FC = () => {
// 从Context中获取全局state和dispatch
const { state, dispatch } = useContext(TodoContext)
// 删除Todo:派发「delete」动作,传递要删除的ID作为payload
function del(id: string) {
dispatch({ type: 'delete', payload: id })
}
return (
<ul>
{/* 遍历state,渲染每个Todo */}
{state.map(todo => (
<li key={todo.id}> {/* key必须唯一,用Todo的id */}
<span>{todo.title}</span>
{/* 点击删除按钮,调用del并传递当前Todo的id */}
<button onClick={() => del(todo.id)}>删除</button>
</li>
))}
</ul>
)
}
export default List
核心逻辑:
• 遍历全局 state 渲染 Todo 列表;
• 每个删除按钮绑定 del 方法,派发 delete 动作并传递目标 ID;
• 同样通过 useContext 获取全局状态和更新方法。
核心用法总结
- useReducer 用法
javascript
const [state, dispatch] = useReducer(reducer函数, 初始状态)
state:当前最新的状态(这里是 Todo 数组);
• dispatch:派发动作的函数,格式:dispatch({ type: '动作名', payload: 附加数据 });
• reducer:根据 action.type 处理不同逻辑,返回新状态(必须保证不可变)。
-
Context 跨组件通信用法
-
创建 Context:createContext(默认值);
-
提供 Context:Context.Provider value={{ 要共享的数据/方法 }};
-
消费 Context:子组件用 useContext(Context) 获取共享内容。
-
不可变数据的常用操作

- 局部状态 vs 全局状态 • 全局状态:
Todo 列表(需要在 List/InputForm 共享)→ 用 useReducer + Context;
• 局部状态:输入框文字(仅 InputForm 用)→ 用 useState。
四、流程梳理(新增 / 删除 Todo)
新增 Todo 流程:
-
用户在 InputForm 输入框输入内容 → handleChange 更新局部状态 text;
-
点击提交按钮 → handleSubmit 校验内容,构建新 Todo 对象;
-
调用 dispatch({ type: 'add', payload: 新Todo });
-
reducer 接收到 add 动作 → 用 concat 返回新的 Todo 数组;
-
useReducer 检测到状态变化 → 更新 state;
-
Context 把新 state 传递给 List 组件 → List 重新渲染,展示新 Todo。
删除 Todo 流程:
-
用户点击 List 中某 Todo 的删除按钮 → 调用 del(id);
-
del 方法调用 dispatch({ type: 'delete', payload: id });
-
reducer 接收到 delete 动作 → 用 filter 返回过滤后的新数组;
-
useReducer 更新 state → List 重新渲染,移除被删除的 Todo。