路由与状态管理
本页关键词:React Router、BrowserRouter、useParams、useContext、createContext、Provider、useReducer、dispatch
一、React Router 基础
React Router 是 React 生态的路由库,与 Vue Router 思路一致但风格不同——Vue Router 是配置式(写 routes 数组),React Router 是组件式(路由本身就是 JSX)。
与 Vue Router 的对比
| Vue Router | React Router | 作用 |
|---|---|---|
<router-view> | <Outlet> / <Routes> | 显示路由匹配的内容 |
<router-link> | <Link> | 导航链接(不刷新页面) |
router.push() | useNavigate() | 编程式导航 |
useRoute().params | useParams() | 获取路由参数 |
routes 配置数组 | <Route> 组件 | 定义路由规则 |
基本用法
import { BrowserRouter, Routes, Route, Link } from "react-router-dom"
import Home from "./pages/Home"
import TodoPage from "./pages/TodoPage"
export default function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">首页</Link> | <Link to="/todos">Todo</Link>
</nav>
<hr />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/todos" element={<TodoPage />} />
</Routes>
</BrowserRouter>
)
}各组件作用:
<BrowserRouter>—— 包裹整个应用,提供路由能力(相当于 Vue 的createRouter)<Routes>—— 路由匹配区域,只有匹配的<Route>会渲染<Route path="..." element={...}>—— 定义 URL 与组件的映射<Link to="...">—— 点击跳转,不刷新页面<nav>写在<Routes>外面 —— 导航栏在所有页面都显示,只有<Routes>内的内容随路由切换
二、路由参数
动态路由参数用 :参数名 定义,通过 useParams() 获取:
// 路由定义
<Route path="/todos/:id" element={<TodoDetail />} />
// 页面组件中获取参数
import { useParams, Link } from "react-router-dom"
export default function TodoDetail() {
const { id } = useParams() // id 是字符串类型
return (
<div>
<h1>Todo 详情</h1>
<p>你正在查看第 {id} 个待办事项</p>
<Link to="/todos">返回列表</Link>
</div>
)
}在列表页通过模板字符串拼接动态路径:
<Link to={`/todos/${index}`}>{todo}</Link>注意:useParams() 返回的参数值都是字符串,需要时要手动转换 Number(id)。
面试要点:React Router 的
:id动态路由参数与 Vue Router 完全一致。useParams返回字符串类型的参数对象。
三、useContext —— 跨组件共享数据
问题场景
当兄弟组件需要共享数据,但无法通过 props 直接传递时:
App
├── TodoPage ← todos 在这里
└── TodoDetail ← 想用 todos,但拿不到TodoPage 和 TodoDetail 是兄弟关系(都是 App 的子路由),无法通过 props 传递。
解决方案:Context
Context 提供了一种在组件树中"广播"数据的机制,无需层层传递 props。
与 Vue 的 provide/inject 对比:
// Vue
provide('todos', todos) // 祖先提供
const todos = inject('todos') // 后代注入
// React
<TodoContext.Provider value={todos}> // 祖先提供
const todos = useContext(TodoContext) // 后代消费完整实现
第一步:创建 Context 和 Provider
// src/TodoContext.tsx
import React, { createContext, useContext, useState, useCallback } from "react"
interface TodoContextType {
todos: string[]
input: string
setInput: (value: string) => void
addTodo: () => void
removeTodo: (index: number) => void
}
const TodoContext = createContext<TodoContextType | null>(null)
export function TodoProvider({ children }: { children: React.ReactNode }) {
const [todos, setTodos] = useState<string[]>([])
const [input, setInput] = useState("")
const addTodo = () => {
if (input.trim() === "") return
setTodos([...todos, input])
setInput("")
}
const removeTodo = useCallback((index: number) => {
setTodos(prev => prev.filter((_, i) => i !== index))
}, [])
return (
<TodoContext.Provider value={{ todos, input, setInput, addTodo, removeTodo }}>
{children}
</TodoContext.Provider>
)
}
export function useTodoContext() {
const context = useContext(TodoContext)
if (!context) throw new Error("useTodoContext must be used within TodoProvider")
return context
}第二步:用 Provider 包裹需要共享数据的组件树
// App.tsx
export default function App() {
return (
<BrowserRouter>
<TodoProvider>
<nav>...</nav>
<Routes>
<Route path="/todos" element={<TodoPage />} />
<Route path="/todos/:id" element={<TodoDetail />} />
</Routes>
</TodoProvider>
</BrowserRouter>
)
}第三步:子组件通过自定义 Hook 消费数据
// TodoPage.tsx
export default function TodoPage() {
const { todos } = useTodoContext()
// ...
}
// TodoDetail.tsx
export default function TodoDetail() {
const { id } = useParams()
const { todos } = useTodoContext()
const todo = todos[Number(id)]
// ...
}关键概念解析
createContext —— 创建一个空容器。
<Provider value={...}> —— 往容器里填数据,被包裹的所有子组件都能拿到。如果组件在 Provider 外面调用 useContext,会拿到 null(或默认值)。
{children} —— React 的特殊 prop,代表标签之间包裹的所有内容。React.ReactNode 类型表示"任何 React 能渲染的东西"。
useTodoContext —— 封装的自定义 Hook,加了错误检查,防止在 Provider 外部误用。
命名导出 vs 默认导出:一个文件导出多个东西时用命名导出(export function),导入时用花括号按需取用:
import { TodoProvider, useTodoContext } from "./TodoContext"面试要点:Context 等价于 Vue 的
provide/inject,用于跨层级传递数据。Provider 是数据的提供者,useContext 是消费者。Context 适合低频更新的全局数据(主题、用户信息),高频更新的数据建议用状态管理库。
四、useReducer —— 集中式状态管理
为什么需要 useReducer
当 state 逻辑变复杂(多种操作:添加、删除、编辑、标记完成……),多个 useState + 散落的操作函数会越来越乱。useReducer 将所有状态变更逻辑集中到一个函数中。
与 Vuex/Pinia 的对比
// Vuex
mutations: {
ADD_TODO(state, payload) { state.todos.push(payload) },
REMOVE_TODO(state, payload) { ... }
}
// React useReducer
function reducer(state, action) {
switch (action.type) {
case "ADD_TODO": return { todos: [...state.todos, action.payload] }
case "REMOVE_TODO": return { todos: state.todos.filter(...) }
}
}思路一致:一个函数,根据 action 类型,决定怎么更新 state。
完整实现
// 1. 定义 state 类型
interface TodoState {
todos: string[]
}
// 2. 定义 action 类型(联合类型,限定所有合法操作)
type TodoAction =
| { type: "ADD_TODO"; payload: string }
| { type: "REMOVE_TODO"; payload: number }
// 3. 定义 reducer 函数(纯函数,接收旧 state + action,返回新 state)
function todoReducer(state: TodoState, action: TodoAction): TodoState {
switch (action.type) {
case "ADD_TODO":
return { todos: [...state.todos, action.payload] }
case "REMOVE_TODO":
return { todos: state.todos.filter((_, i) => i !== action.payload) }
default:
return state
}
}
// 4. 在组件中使用
export function TodoProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(todoReducer, { todos: [] })
const [input, setInput] = useState("")
const addTodo = () => {
if (input.trim() === "") return
dispatch({ type: "ADD_TODO", payload: input })
setInput("")
}
const removeTodo = (index: number) => {
dispatch({ type: "REMOVE_TODO", payload: index })
}
return (
<TodoContext.Provider value={{ todos: state.todos, input, setInput, addTodo, removeTodo }}>
{children}
</TodoContext.Provider>
)
}核心概念
useReducer(reducer, initialState) 返回 [state, dispatch],与 useState 对比:
const [count, setCount] = useState(0) // 简单值,直接 set
const [state, dispatch] = useReducer(todoReducer, { todos: [] }) // 复杂逻辑,发 actiondispatch —— 发送指令,不直接修改 state:
dispatch({ type: "ADD_TODO", payload: "买菜" })
// ↑ 告诉 reducer 做什么 ↑ 携带的数据reducer —— 纯函数,接收当前 state 和 action,返回新的 state。React 用返回值替换旧 state。
dispatch 的引用是稳定的 —— React 保证 dispatch 永远不变,不需要 useCallback 包裹。
TypeScript 类型说明
type 关键字定义类型别名,| 表示联合类型("或"):
type TodoAction =
| { type: "ADD_TODO"; payload: string } // 添加操作,payload 是字符串
| { type: "REMOVE_TODO"; payload: number } // 删除操作,payload 是数字TypeScript 会在编译时检查 dispatch 的参数是否合法:
dispatch({ type: "HAHA", payload: 123 }) // ✗ 报错,没有这个 type
dispatch({ type: "ADD_TODO", payload: 123 }) // ✗ 报错,payload 应该是 stringinterface 定义对象结构,编译时检查,运行时不存在(会被编译掉)。type 和 interface 都能定义类型,interface 适合对象结构,type 更灵活(支持联合类型、交叉类型等)。
什么时候用 useReducer
| 场景 | 选择 |
|---|---|
| state 是简单值(数字、字符串、布尔) | useState |
| state 是复杂结构,有多种操作方式 | useReducer |
| 多个 state 之间有关联 | useReducer |
面试要点:
useReducer是useState的替代方案,适合复杂状态逻辑。核心三要素:state(当前状态)、action(操作指令)、reducer(纯函数,计算新状态)。dispatch引用稳定,不需要useCallback。
五、数据流全景
以 Todo 应用为例,整体数据流:
createContext → 创建空容器
TodoProvider → 组件,负责往容器里填数据,包裹子组件
├── useReducer → 管理 todos 状态,返回 [state, dispatch]
├── useState → 管理 input 状态
├── addTodo → 调用 dispatch 发送 ADD_TODO
├── removeTodo → 调用 dispatch 发送 REMOVE_TODO
└── Provider → 把数据广播给所有子组件
todoReducer → 接收指令,计算新 state
useTodoContext → 自定义 Hook,子组件用它取数据完整数据流:子组件调用 addTodo → addTodo 调用 dispatch → dispatch 调用 todoReducer → reducer 返回新 state → React 更新 → 所有消费了 Context 的组件拿到新数据 → re-render。
六、React 核心 API 总览
| Hook / API | 作用 | 对应 Vue 概念 |
|---|---|---|
useState | 声明响应式状态 | ref / reactive |
useEffect | 处理副作用 | onMounted / watch |
useContext | 跨组件共享数据 | inject |
useCallback | 缓存函数引用 | 无直接对应 |
useReducer | 集中式状态管理 | Vuex/Pinia 的 reducer 思想 |
React.memo | 跳过不必要的 re-render | 无需(Vue 默认精确更新) |
| 自定义 Hook | 逻辑复用 | Composable(useXxx) |
| React Router | 路由管理 | Vue Router |
掌握以上内容,React 核心基础完整覆盖。后续的 Zustand、React Query、Next.js 等属于生态工具,按需选学。