Skip to content

路由与状态管理

本页关键词: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 RouterReact Router作用
<router-view><Outlet> / <Routes>显示路由匹配的内容
<router-link><Link>导航链接(不刷新页面)
router.push()useNavigate()编程式导航
useRoute().paramsuseParams()获取路由参数
routes 配置数组<Route> 组件定义路由规则

基本用法

tsx
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() 获取:

tsx
// 路由定义
<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>
  )
}

在列表页通过模板字符串拼接动态路径:

tsx
<Link to={`/todos/${index}`}>{todo}</Link>

注意useParams() 返回的参数值都是字符串,需要时要手动转换 Number(id)

面试要点:React Router 的 :id 动态路由参数与 Vue Router 完全一致。useParams 返回字符串类型的参数对象。


三、useContext —— 跨组件共享数据

问题场景

当兄弟组件需要共享数据,但无法通过 props 直接传递时:

App
├── TodoPage    ← todos 在这里
└── TodoDetail  ← 想用 todos,但拿不到

TodoPageTodoDetail 是兄弟关系(都是 App 的子路由),无法通过 props 传递。

解决方案:Context

Context 提供了一种在组件树中"广播"数据的机制,无需层层传递 props。

与 Vue 的 provide/inject 对比:

javascript
// Vue
provide('todos', todos)           // 祖先提供
const todos = inject('todos')     // 后代注入

// React
<TodoContext.Provider value={todos}>  // 祖先提供
const todos = useContext(TodoContext)  // 后代消费

完整实现

第一步:创建 Context 和 Provider

tsx
// 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 包裹需要共享数据的组件树

tsx
// 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 消费数据

tsx
// 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),导入时用花括号按需取用:

tsx
import { TodoProvider, useTodoContext } from "./TodoContext"

面试要点:Context 等价于 Vue 的 provide/inject,用于跨层级传递数据。Provider 是数据的提供者,useContext 是消费者。Context 适合低频更新的全局数据(主题、用户信息),高频更新的数据建议用状态管理库。


四、useReducer —— 集中式状态管理

为什么需要 useReducer

当 state 逻辑变复杂(多种操作:添加、删除、编辑、标记完成……),多个 useState + 散落的操作函数会越来越乱。useReducer 将所有状态变更逻辑集中到一个函数中。

与 Vuex/Pinia 的对比

javascript
// 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。

完整实现

tsx
// 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 对比:

tsx
const [count, setCount] = useState(0)                          // 简单值,直接 set
const [state, dispatch] = useReducer(todoReducer, { todos: [] })  // 复杂逻辑,发 action

dispatch —— 发送指令,不直接修改 state:

tsx
dispatch({ type: "ADD_TODO", payload: "买菜" })
//         ↑ 告诉 reducer 做什么    ↑ 携带的数据

reducer —— 纯函数,接收当前 state 和 action,返回新的 state。React 用返回值替换旧 state。

dispatch 的引用是稳定的 —— React 保证 dispatch 永远不变,不需要 useCallback 包裹。

TypeScript 类型说明

type 关键字定义类型别名,| 表示联合类型("或"):

tsx
type TodoAction =
  | { type: "ADD_TODO"; payload: string }    // 添加操作,payload 是字符串
  | { type: "REMOVE_TODO"; payload: number } // 删除操作,payload 是数字

TypeScript 会在编译时检查 dispatch 的参数是否合法:

tsx
dispatch({ type: "HAHA", payload: 123 })       // ✗ 报错,没有这个 type
dispatch({ type: "ADD_TODO", payload: 123 })    // ✗ 报错,payload 应该是 string

interface 定义对象结构,编译时检查,运行时不存在(会被编译掉)。typeinterface 都能定义类型,interface 适合对象结构,type 更灵活(支持联合类型、交叉类型等)。

什么时候用 useReducer

场景选择
state 是简单值(数字、字符串、布尔)useState
state 是复杂结构,有多种操作方式useReducer
多个 state 之间有关联useReducer

面试要点useReduceruseState 的替代方案,适合复杂状态逻辑。核心三要素: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,子组件用它取数据

完整数据流:子组件调用 addTodoaddTodo 调用 dispatchdispatch 调用 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 等属于生态工具,按需选学。