React 生态周边

1,158 阅读5分钟

一、Redux 完整指南

🎯 什么是Redux?为什么需要它?

Redux 是一个用于JavaScript应用的状态容器,提供可预测化的状态管理。它基于Flux架构,但更简洁、更强大。

// Redux的核心思想:整个应用的状态存储在一个对象树中
console.log(store.getState()) // { user: {...}, posts: [...], ui: {...} }

为什么要用Redux?🤔

前端复杂性的根本原因是大量无规律的交互异步操作。随着应用规模增大,我们会面临:

  1. 组件间通信混乱:父子、兄弟、跨层级通信错综复杂
  2. 状态变化难以追踪:不知道状态何时、为何、如何变化
  3. 调试困难:Bug复现困难,无法回溯操作流程

Redux通过单向数据流集中式状态管理解决了这些问题:

视图层触发Action → Dispatch分发 → Reducer计算新状态 → 视图自动更新
  • 适用场景:多交互、多数据源、状态共享频繁的中大型应用
  • 不适用场景:简单应用(Context就够了)、静态页面

🏗️ Redux核心概念详解

1️⃣ Store(状态容器)

Store是Redux的核心,它是唯一的数据源,整个应用只能有一个Store。

import { createStore } from 'redux'

// 传统方式(Redux Toolkit之前)
const store = createStore(rootReducer)

// Store提供的方法
store.getState()      // 获取当前状态
store.dispatch(action) // 分发action
store.subscribe(listener) // 订阅状态变化

源码简析:createStore内部使用闭包维护state,通过dispatch触发reducer更新,并通知所有订阅者。

2️⃣ Action(动作)

Action是一个普通的JavaScript对象,描述“发生了什么”。

// Action的基本结构
const addTodoAction = {
  type: 'todos/todoAdded', // 必须的type字段
  payload: 'Buy milk'      // 携带的数据(命名规范)
}

// Action Creator:方便复用
const addTodo = (text) => ({
  type: 'todos/todoAdded',
  payload: text
})

// 使用
dispatch(addTodo('学习Redux'))

3️⃣ Reducer(归约函数)

Reducer是纯函数,接收当前State和Action,返回新State。

// Reducer的基本形式 (state, action) => newState
const initialState = {
  todos: [],
  status: 'idle'
}

function todosReducer(state = initialState, action) {
  switch (action.type) {
    case 'todos/todoAdded':
      // ❗重要:不能直接修改state,要返回新对象
      return {
        ...state,
        todos: [...state.todos, action.payload]
      }
    case 'todos/todoToggled':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload 
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      }
    default:
      return state
  }
}

为什么叫Reducer? 因为这种函数模式与Array.prototype.reduce()的回调函数类似——接收上一个状态和动作,累积计算出新状态。

  • 拆分:根据独立的模块拆分出单独的 reducer

  • 合并:创建 Store 时合并 reducer

  • 统一管理 actionType

🔄 Redux数据流完整流程

Redux采用单向数据流,整个过程清晰可预测:

1. 用户交互
    👇
2. dispatch(action) 
    👇
3. Store调用Reducer 
    👇
4. 计算新State 
    👇
5. 通知订阅者
    👇
6. 更新UI
// 完整示例
import { createStore } from 'redux'

// 1. 定义reducer
function counterReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 }
    default:
      return state
  }
}

// 2. 创建store
const store = createStore(counterReducer)

// 3. 订阅变化
store.subscribe(() => {
  console.log('当前状态:', store.getState())
})

// 4. 分发action
store.dispatch({ type: 'INCREMENT' }) // 输出:当前状态: { count: 1 }

🚀 现代Redux:Redux Toolkit(RTK)

为什么需要RTK?

传统Redux有三大痛点:

  • 样板代码过多:Action Types、Action Creators、Reducer分开写
  • 配置复杂:需要手动配置中间件、DevTools
  • 不可变导致更新繁琐:必须使用对象展开/数组map,容易出错

Redux Toolkit 是官方推荐的现代Redux开发工具集,解决了上述所有问题。

1️⃣ configureStore - 简化Store配置

import { configureStore } from '@reduxjs/toolkit'
import todosReducer from './features/todos/todosSlice'

// ✅ 一行搞定所有配置!
const store = configureStore({
  reducer: {
    todos: todosReducer,
    // 可以添加更多reducer
  }
})

// configureStore自动做了这些事:
// - 合并reducer
// - 添加thunk中间件
// - 启用Redux DevTools
// - 添加开发环境检查(状态可变性、序列化等)

2️⃣ createSlice - 革命性的Slice概念

createSlice将Action和Reducer合并到一个文件中,极大减少样板代码。

import { createSlice } from '@reduxjs/toolkit'

const todosSlice = createSlice({
  name: 'todos',
  initialState: {
    items: [],
    status: 'idle'
  },
  reducers: {
    // ✅ 直接使用"可变"语法,内部使用Immer自动处理不可变更新
    todoAdded(state, action) {
      state.items.push({
        id: Date.now(),
        text: action.payload,
        completed: false
      })
    },
    todoToggled(state, action) {
      const todo = state.items.find(todo => todo.id === action.payload)
      if (todo) {
        todo.completed = !todo.completed
      }
    },
    todoDeleted(state, action) {
      state.items = state.items.filter(todo => todo.id !== action.payload)
    }
  }
})

// ✅ 自动生成action creators
export const { todoAdded, todoToggled, todoDeleted } = todosSlice.actions

// ✅ 自动生成reducer
export default todosSlice.reducer

3️⃣ 在组件中使用Redux(React-Redux Hooks)

现代Redux推荐使用Hooks API替代connect高阶组件。

import { useSelector, useDispatch } from 'react-redux'
import { todoAdded, todoToggled } from './todosSlice'

function TodoList() {
  // ✅ useSelector:从store提取数据
  const todos = useSelector(state => state.todos.items)
  const dispatch = useDispatch() // ✅ useDispatch:获取dispatch函数
  
  const handleAddTodo = (text) => {
    dispatch(todoAdded(text)) // 直接调用action creator
  }
  
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => dispatch(todoToggled(todo.id))}
          />
          {todo.text}
        </li>
      ))}
    </ul>
  )
}

⚡ 异步处理:Redux中间件

中间件原理

中间件提供第三方扩展点,在dispatch action和到达reducer之间拦截并插入代码。

// 中间件的签名:store => next => action => {}
const loggerMiddleware = store => next => action => {
  console.log('dispatching:', action)
  console.log('prev state:', store.getState())
  
  const result = next(action) // 传递给下一个中间件或reducer
  
  console.log('next state:', store.getState())
  return result
}

// 应用中间件
import { createStore, applyMiddleware } from 'redux'
const store = createStore(rootReducer, applyMiddleware(loggerMiddleware))

常用异步中间件

Redux Thunk(官方推荐)

Thunk允许action是一个函数,而不仅仅是对象。

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'

// ✅ RTK的createAsyncThunk自动处理异步生命周期
export const fetchTodos = createAsyncThunk(
  'todos/fetchTodos',
  async (userId, thunkAPI) => {
    const response = await fetch(`/api/todos?userId=${userId}`)
    return response.json() // 返回的数据会成为action.payload
  }
)

const todosSlice = createSlice({
  name: 'todos',
  initialState: {
    items: [],
    status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
    error: null
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.pending, (state) => {
        state.status = 'loading'
      })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.status = 'succeeded'
        state.items = action.payload
      })
      .addCase(fetchTodos.rejected, (state, action) => {
        state.status = 'failed'
        state.error = action.error.message
      })
  }
})

// 在组件中使用
dispatch(fetchTodos(123))
Redux Saga(适合复杂异步流)

Saga使用ES6的Generator函数,让异步流程更易管理。

import { call, put, takeLatest } from 'redux-saga/effects'

function* fetchUser(action) {
  try {
    const user = yield call(api.fetchUser, action.payload.userId)
    yield put({ type: 'FETCH_USER_SUCCESS', user })
  } catch (error) {
    yield put({ type: 'FETCH_USER_FAILURE', error })
  }
}

function* watchFetchUser() {
  yield takeLatest('FETCH_USER_REQUEST', fetchUser)
}

🧠 性能优化技巧

1️⃣ 使用Reselect创建记忆化选择器

问题:每次useSelector都会执行,即使状态没变也可能导致重渲染。

import { createSelector } from '@reduxjs/toolkit'

// ✅ 记忆化选择器:只有相关状态变化时才重新计算
const selectTodos = state => state.todos.items
const selectFilter = state => state.todos.filter

export const selectFilteredTodos = createSelector(
  [selectTodos, selectFilter],
  (todos, filter) => {
    console.log('计算过滤后的todos') // 只有todos或filter变化时才执行
    switch (filter) {
      case 'active':
        return todos.filter(todo => !todo.completed)
      case 'completed':
        return todos.filter(todo => todo.completed)
      default:
        return todos
    }
  }
)

// 组件中使用
const filteredTodos = useSelector(selectFilteredTodos)

2️⃣ 避免不必要的重渲染

// ❌ 每次渲染都会创建新对象,导致子组件重渲染
function BadComponent() {
  const todo = useSelector(state => ({
    id: state.todos.selectedId,
    text: state.todos.items[state.todos.selectedId]?.text
  }))
  return <TodoItem todo={todo} />
}

// ✅ 使用useSelector的浅比较或拆分为多个useSelector
function GoodComponent() {
  const id = useSelector(state => state.todos.selectedId)
  const text = useSelector(state => state.todos.items[id]?.text)
  
  // 或者使用相等性比较函数
  const todo = useSelector(state => ({
    id: state.todos.selectedId,
    text: state.todos.items[state.todos.selectedId]?.text
  }), shallowEqual) // react-redux的浅比较
  
  return <TodoItem todo={todo} />
}

3️⃣ 规范化State结构

避免深层嵌套,尽量扁平化存储。

// ❌ 嵌套结构
{
  posts: [{
    id: 1,
    title: '...',
    author: {
      id: 101,
      name: '张三',
      posts: [...]
    }
  }]
}

// ✅ 规范化:按ID存储,关系通过ID引用
{
  posts: {
    byId: {
      1: { id: 1, title: '...', authorId: 101 }
    },
    allIds: [1]
  },
  users: {
    byId: {
      101: { id: 101, name: '张三' }
    },
    allIds: [101]
  }
}

💣 常见坑点与解决方案

1️⃣ 状态直接修改(Immutability)

// ❌ 错误:直接修改state
function todosReducer(state, action) {
  state.todos.push(action.payload) // 不会触发更新!
  return state
}

// ✅ 正确:返回新对象
function todosReducer(state, action) {
  return {
    ...state,
    todos: [...state.todos, action.payload]
  }
}

// ✅ 更简单:使用Redux Toolkit(自动处理)
const todosSlice = createSlice({
  name: 'todos',
  initialState: { todos: [] },
  reducers: {
    todoAdded(state, action) {
      state.todos.push(action.payload) // 安全!Immer自动处理
    }
  }
})

2️⃣ 异步action的类型匹配问题

// ❌ 错误:id类型不匹配导致filter失败
const id = useParams().id // 字符串 "123"
const user = users.userList.filter(f => f.id === id) // 如果f.id是数字,匹配失败

// ✅ 正确:确保类型一致
const id = parseInt(useParams().id) // 转换为数字
const user = users.userList.filter(f => f.id === id)

3️⃣ 表单状态与Redux同步问题

// ❌ 错误:dispatch发送的是初始值,不是当前输入值
function UpdateUser() {
  const user = useSelector(state => state.users.selected)
  const [name, setName] = useState(user.name)
  
  const handleSubmit = () => {
    dispatch(updateUser({ id: user.id, name: user.name })) // ❌ 用了初始值
  }
  
  // ✅ 正确:使用当前输入值
  const handleSubmitCorrect = () => {
    dispatch(updateUser({ id: user.id, name })) // 用useState的当前值
  }
}

4️⃣ 选择器依赖props时的陈年bug

// ❌ 问题:选择器依赖props,但组件可能用旧的props执行选择器
function Post({ postId }) {
  const post = useSelector(state => state.posts.byId[postId])
  // 如果postId变化但组件还没重渲染,选择器可能用旧postId执行
}

// ✅ 解决:确保选择器在依赖变化时重新计算
const makeSelectPostById = () => 
  createSelector(
    state => state.posts.byId,
    (_, postId) => postId,
    (byId, postId) => byId[postId]
  )

function Post({ postId }) {
  const selectPostById = useMemo(makeSelectPostById, [])
  const post = useSelector(state => selectPostById(state, postId))
}

🎯 难点解析

Q1:Redux的核心原则是什么?

  1. 单一数据源:整个应用只有唯一Store
  2. State只读:唯一改变state的方式是触发action
  3. 使用纯函数修改:reducer必须是纯函数,接收旧state和action,返回新state

Q2:Redux和Flux的区别?

维度FluxRedux
Store数量多个Store单一Store
Dispatcher有Dispatcher无Dispatcher,reducer直接处理
状态更新在Store中更新在Reducer中更新
开发体验一般支持时间旅行、热重载

Q3:Redux中间件的实现原理?

中间件是对dispatch函数的增强,通过函数式编程的柯里化实现:

// 中间件签名
const middleware = store => next => action => {
  // 在action到达reducer前做点什么
  const result = next(action) // 传递给下一个中间件或reducer
  // 在reducer执行后做点什么
  return result
}

// 应用中间件
dispatch = middleware1(store)(middleware2(store)(store.dispatch))

Q4:Redux Toolkit相比传统Redux的优势?

  1. 减少样板代码createSlice合并action/reducer
  2. 自动处理不可变更新:Immer集成,可直接写"可变"语法
  3. 内置异步支持createAsyncThunk简化异步action
  4. 默认最佳实践:自动配置中间件、DevTools
  5. TypeScript友好:类型推断更智能

Q5:如何调试Redux应用?

Redux DevTools提供强大功能:

  • 时间旅行:查看每一步action前后的状态变化
  • action回溯:重新执行任何历史action
  • 状态快照:导出/导入应用状态
  • Diff对比:直观看到状态变化
// RTK自动启用DevTools,无需额外配置
const store = configureStore({
  reducer: rootReducer,
  devTools: process.env.NODE_ENV !== 'production'
})

二、React Router完全指南

🎯 什么是React Router?为什么需要它?

官方定义

React Router是React生态中最流行的路由解决方案,它让你在单页应用(SPA)中实现多视图切换,同时保持UI与URL的同步。

// 最简单的路由示例
import { BrowserRouter, Routes, Route } from 'react-router-dom'

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  )
}

为什么需要路由?🤔

在SPA出现之前,每次页面切换都要向服务器发送请求,导致:

  • ❌ 页面刷新,体验割裂
  • ❌ 重复加载公共资源
  • ❌ 无法保持应用状态

React Router解决了这些问题

  • 无刷新切换:页面局部更新,体验流畅
  • URL与视图同步:支持前进后退、分享链接
  • 代码分割:按需加载,性能优化

🏗️ 核心原理:React Router是如何工作的?

1️⃣ 两种路由模式

React Router基于history库实现两种路由模式:

import { BrowserRouter, HashRouter } from 'react-router-dom'

// 🔵 BrowserRouter(推荐)
// URL格式:https://example.com/users/123
// 基于HTML5 History API
<BrowserRouter>
  <App />
</BrowserRouter>

// 🟢 HashRouter
// URL格式:https://example.com/#/users/123
// 基于URL的hash部分
<HashRouter>
  <App />
</HashRouter>

核心区别

特性BrowserRouterHashRouter
URL格式标准路径 /users带#号 /#/users
原理history.pushState + popstatewindow.onhashchange
服务器配置需要配置所有路径返回index.html无需特殊配置
SEO友好✅ 更好❌ 较差
兼容性IE10+所有浏览器

2️⃣ 路由匹配原理

React Router的核心机制是:监听URL变化 → 匹配路由规则 → 渲染对应组件

// 简化版原理实现
function matchPath(pathname, routes) {
  for (let route of routes) {
    // 使用path-to-regexp库将路径模式转为正则
    const regex = pathToRegexp(route.path)
    const match = regex.exec(pathname)
    
    if (match) {
      return {
        path: route.path,
        url: match[0],
        params: extractParams(match, route.path) // { id: '123' }
      }
    }
  }
  return null
}

// v6的核心改进:智能路由匹配,不再需要exact属性
// v5需要exact避免匹配冲突
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />

// v6自动选择最佳匹配
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />

3️⃣ 内部工作流程

// React Router的完整工作流程
// 1. 用户点击Link组件
<Link to="/users/123">用户详情</Link>

// 2. Link阻止a标签默认行为,调用history.push
// 内部实现简化版
function Link({ to, children }) {
  const navigate = useNavigate()
  
  const handleClick = (e) => {
    e.preventDefault() // 阻止页面刷新
    navigate(to) // 编程式导航
  }
  
  return <a href={to} onClick={handleClick}>{children}</a>
}

// 3. history库更新URL并触发监听
window.history.pushState({}, '', to)
window.dispatchEvent(new PopStateEvent('popstate'))

// 4. Router组件监听到变化,重新匹配路由
useEffect(() => {
  const unlisten = history.listen(({ location }) => {
    // 更新上下文中的location
    setLocation(location)
  })
  return unlisten
}, [])

// 5. Routes遍历Route,渲染匹配的组件
<Routes>
  <Route path="/users/:id" element={<UserDetail />} />
</Routes>

🚀 React Router v6 核心实战

1️⃣ 基础路由配置

// App.js
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'

function App() {
  return (
    <BrowserRouter>
      {/* 导航菜单 */}
      <nav>
        <Link to="/">首页</Link>
        <Link to="/about">关于</Link>
        <Link to="/users">用户列表</Link>
      </nav>

      {/* 路由配置区域 */}
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/users" element={<UserList />} />
        {/* 404页面:放在最后 */}
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  )
}

2️⃣ 动态路由传参(3种方式)

// ============ 方式1:动态参数(URL参数)============
// 适合:详情页、需要唯一标识的场景

// 路由配置
<Route path="/user/:id" element={<UserDetail />} />

// 传参组件
function UserList() {
  return (
    <Link to="/user/123">查看用户123</Link>
  )
}

// 接收参数组件
import { useParams } from 'react-router-dom'

function UserDetail() {
  const { id } = useParams() // { id: '123' }
  // ⚠️ 注意:id是字符串!需要数字请手动转换
  const userId = Number(id)
  
  return <div>用户ID:{userId}</div>
}

// ============ 方式2:查询参数(Query String)============
// 适合:筛选、分页、搜索条件

// 传参
<Link to="/users?page=2&size=10">第二页</Link>

// 接收参数
import { useSearchParams } from 'react-router-dom'

function UserList() {
  const [searchParams, setSearchParams] = useSearchParams()
  const page = searchParams.get('page') || '1'
  const size = searchParams.get('size') || '10'
  
  const goToPage2 = () => {
    setSearchParams({ page: 2, size: 10 })
  }
  
  return (
    <div>
      <p>第{page}页,每页{size}条</p>
      <button onClick={goToPage2}>跳转到第2页</button>
    </div>
  )
}

// ============ 方式3:state参数(隐式传参)============
// 适合:传递敏感数据、复杂对象(⚠️刷新页面会丢失)

// 传参
import { useNavigate } from 'react-router-dom'

function Login() {
  const navigate = useNavigate()
  const userInfo = { id: 123, name: '张三', token: 'xxx' }
  
  const handleLogin = () => {
    // 通过state传递用户信息
    navigate('/dashboard', { state: { user: userInfo } })
  }
  
  return <button onClick={handleLogin}>登录</button>
}

// 接收参数
import { useLocation } from 'react-router-dom'

function Dashboard() {
  const location = useLocation()
  const { user } = location.state || {} // 避免undefined
  
  return <div>欢迎回来,{user?.name}</div>
}

3️⃣ 嵌套路由

// v6的嵌套路由更加简洁,使用Outlet组件

// ============ 主路由配置 ============
import { Routes, Route } from 'react-router-dom'
import Layout from './Layout'
import Dashboard from './Dashboard'
import Profile from './Profile'
import Settings from './Settings'

function App() {
  return (
    <Routes>
      {/* 父路由:用*表示深度匹配 */}
      <Route path="/dashboard/*" element={<Layout />}>
        {/* index路由:默认子路由(path为空) */}
        <Route index element={<DashboardHome />} />
        {/* 子路由:相对路径(不需要/dashboard/profile) */}
        <Route path="profile" element={<Profile />} />
        <Route path="settings" element={<Settings />} />
      </Route>
    </Routes>
  )
}

// ============ Layout组件(父组件)============
import { Outlet, Link } from 'react-router-dom'

function Layout() {
  return (
    <div>
      <h1>控制台</h1>
      <nav>
        <Link to="">首页</Link>        {/* 相当于/dashboard */}
        <Link to="profile">个人资料</Link> {/* 相当于/dashboard/profile */}
        <Link to="settings">设置</Link>   {/* 相当于/dashboard/settings */}
      </nav>
      
      {/* ⭐ Outlet:子路由的占位符 */}
      <Outlet />
    </div>
  )
}

4️⃣ 编程式导航

import { useNavigate } from 'react-router-dom'

function NavigationDemo() {
  const navigate = useNavigate()
  
  const handleActions = () => {
    // 1. 基础跳转
    navigate('/about')
    
    // 2. 带参数跳转
    navigate('/user/123')
    
    // 3. 带state
    navigate('/dashboard', { state: { from: 'login' } })
    
    // 4. 替换当前记录(不能返回上一页)
    navigate('/login', { replace: true })
    
    // 5. 后退/前进
    navigate(-1) // 后退
    navigate(1)  // 前进
  }
  
  return <button onClick={handleActions}>导航</button>
}

5️⃣ 路由重定向

import { Navigate, Routes, Route } from 'react-router-dom'

// ============ 方式1:声明式重定向 ============
function App() {
  const isLoggedIn = false
  
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/old-about" element={<Navigate to="/about" replace />} />
      <Route 
        path="/protected" 
        element={
          isLoggedIn ? 
            <ProtectedPage /> : 
            <Navigate to="/login" state={{ from: '/protected' }} />
        } 
      />
    </Routes>
  )
}

// ============ 方式2:编程式重定向 ============
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'

function AuthGuard({ children }) {
  const navigate = useNavigate()
  const token = localStorage.getItem('token')
  
  useEffect(() => {
    if (!token) {
      // 未登录,重定向到登录页
      navigate('/login', { replace: true })
    }
  }, [token, navigate])
  
  return token ? children : null
}

6️⃣ 路由守卫(权限控制)

// v6的路由守卫实现:封装受保护路由组件

// ============ 高阶组件方式 ============
function RequireAuth({ children }) {
  const token = localStorage.getItem('token')
  const location = useLocation()
  
  if (!token) {
    // 重定向到登录页,并记录来源,登录后可以跳回
    return <Navigate to="/login" state={{ from: location }} replace />
  }
  
  return children
}

// 使用
<Route 
  path="/dashboard" 
  element={
    <RequireAuth>
      <Dashboard />
    </RequireAuth>
  } 
/>

// ============ 路由配置数组方式(useRoutes)============
import { useRoutes } from 'react-router-dom'

const routeConfig = [
  {
    path: '/',
    element: <Home />
  },
  {
    path: '/dashboard',
    element: (
      <RequireAuth>
        <Dashboard />
      </RequireAuth>
    ),
    children: [
      { path: 'profile', element: <Profile /> },
      { path: 'settings', element: <Settings /> }
    ]
  },
  {
    path: '/login',
    element: <Login />
  },
  {
    path: '*',
    element: <NotFound />
  }
]

function App() {
  const element = useRoutes(routeConfig)
  return element
}

⚡ 性能优化技巧

1️⃣ 路由懒加载(代码分割)

import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'

// 懒加载组件
const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))
const UserDetail = lazy(() => import('./pages/UserDetail'))

function App() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/user/:id" element={<UserDetail />} />
      </Routes>
    </Suspense>
  )
}

2️⃣ 记忆化组件

import { memo } from 'react'

// 防止不必要的重渲染
const NavLink = memo(({ to, children }) => {
  console.log('NavLink渲染')
  return <Link to={to}>{children}</Link>
})

3️⃣ 避免动态创建组件

// ❌ 错误:每次渲染都创建新组件
function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/user/:id" element={<User />} />
      {/* 内联函数会导致每次重新创建 */}
      <Route path="/about" element={() => <About />} />
    </Routes>
  )
}

// ✅ 正确:组件引用保持不变
function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/user/:id" element={<User />} />
      <Route path="/about" element={<About />} />
    </Routes>
  )
}

💣 常见坑点与解决方案

1️⃣ v5升级v6的兼容性问题

// 问题:v5 + React 18导致路由不更新
// 症状:点击链接URL变了,但页面内容不变

// 解决方案:升级到v6,API变化如下

// v5写法
import { Switch, Route, Redirect } from 'react-router-dom'

<Switch>
  <Route exact path="/" component={Home} />
  <Route path="/about" component={About} />
  <Redirect from="/old" to="/new" />
</Switch>

// v6写法
import { Routes, Route, Navigate } from 'react-router-dom'

<Routes>
  <Route path="/" element={<Home />} />  {/* exact没了 */}
  <Route path="/about" element={<About />} />
  <Route path="/old" element={<Navigate to="/new" replace />} />
</Routes>

2️⃣ state参数刷新丢失

// 问题:通过state传递的数据,刷新页面就没了

// 解决方案1:存到localStorage
function Login() {
  const navigate = useNavigate()
  
  const handleLogin = async () => {
    const user = await login()
    // 同时存到localStorage
    localStorage.setItem('user', JSON.stringify(user))
    navigate('/dashboard', { state: { user } })
  }
}

function Dashboard() {
  const location = useLocation()
  // 优先用state,没有则从localStorage读取
  const user = location.state?.user || 
               JSON.parse(localStorage.getItem('user') || '{}')
}

// 解决方案2:使用URL参数(推荐)
// 只传ID,组件内根据ID重新请求数据
<Route path="/user/:id" element={<UserDetail />} />

3️⃣ 动态路由冲突

// 问题:/user/list 和 /user/:id 同时存在时匹配错误

// ❌ 错误顺序
<Routes>
  <Route path="/user/:id" element={<UserDetail />} />
  <Route path="/user/list" element={<UserList />} /> {/* 永远匹配不到! */}
</Routes>

// ✅ 正确顺序(将更具体的路由放在前面)
<Routes>
  <Route path="/user/list" element={<UserList />} />
  <Route path="/user/:id" element={<UserDetail />} />
</Routes>

// 或者在v6中用*表示通配
<Route path="/user/*" element={<UserLayout />}>
  {/* 子路由中处理 */}
</Route>

4️⃣ 与Redux配合时的更新问题

// 问题:connect包裹的组件接收不到路由props变化

// ❌ 错误:connect阻止了props更新
export default connect(mapStateToProps)(UserDetail)

// ✅ 正确:使用hooks方式
import { useSelector } from 'react-redux'
import { useParams } from 'react-router-dom'

function UserDetail() {
  const { id } = useParams() // 直接从hooks获取路由参数
  const user = useSelector(state => state.users[id])
  return <div>{user.name}</div>
}

// 如果必须用connect,使用withRouter包装
import { withRouter } from 'react-router-dom' // v5
export default withRouter(connect(mapStateToProps)(UserDetail))

🎯 难点解析

Q1:React Router的实现原理是什么?

满分回答: React Router基于history库实现,核心原理是:

  1. 两种模式

    • BrowserRouter:利用HTML5 History API(pushState、replaceState、popstate事件)
    • HashRouter:利用URL的hash部分和hashchange事件
  2. 工作流程

    • 点击Link组件,阻止a标签默认行为
    • 通过history API更新URL
    • 触发监听事件,Router组件感知变化
    • 根据新的location匹配路由规则
    • 渲染对应的组件

Q2:Link和a标签的区别?

维度Linka标签
页面刷新无刷新切换会刷新页面
原理阻止默认事件 + history.pushState浏览器原生跳转
性能只更新变化部分重新加载整个页面
状态保持保持组件状态状态丢失

Q3:v5到v6的主要API变化?

// 1. Switch → Routes
<Switch> → <Routes>

// 2. component/render → element
<Route component={Home} /> → <Route element={<Home />} />

// 3. Redirect → Navigate
<Redirect to="/home" /> → <Navigate to="/home" />

// 4. 移除了exact(自动精确匹配)
<Route exact path="/" /> → <Route path="/" />

// 5. 嵌套路由简化
// v5:在子组件中再写Switch
// v6:使用Outlet占位符

Q4:如何获取路由参数?

// 1. 动态参数(/user/:id)
const { id } = useParams()

// 2. 查询参数(?page=1)
const [searchParams] = useSearchParams()
const page = searchParams.get('page')

// 3. state参数
const location = useLocation()
const state = location.state

Q5:如何实现路由权限控制?

function PrivateRoute({ children }) {
  const token = localStorage.getItem('token')
  const location = useLocation()
  
  if (!token) {
    return <Navigate to="/login" state={{ from: location }} replace />
  }
  
  return children
}

// 使用
<Route path="/dashboard" element={
  <PrivateRoute>
    <Dashboard />
  </PrivateRoute>
} />

📊 版本对比速查表

特性v5v6
核心组件SwitchRoutes
路由渲染component/renderelement
精确匹配exact属性自动精确
重定向RedirectNavigate
嵌套路由组件内配置Outlet组件
相对路由需要match.path自动相对
类型支持一般大幅增强

React Router核心价值

声明式路由:像写组件一样写路由 ✅ 嵌套路由:页面结构清晰 ✅ 多种传参:满足各种场景 ✅ 懒加载支持:性能优化 ✅ 完整类型支持:TypeScript友好

三、 React 知识点延伸

1. 不能在循环或条件语句中使用 Hook

函数组件本身是没有状态的,所以需要引入 Hook 为其增加内部状态。

React 中每一个 Hook 方法都对应一个hook对象,用于存储和相关信息。在当前运行组件中有一个_hooks链表用于保存所有的 hook 对象。

export type Hook = {
  memoizedState: any, // 上次渲染时所用的 state
  baseState: any, // 已处理的 update 计算出的 state
  baseQueue: Update<any, any> | null, // 未处理的 update 队列(一般是上一轮渲染未完成的 update)
  queue: UpdateQueue<any, any> | null, // 当前出发的 update 队列
  next: Hook | null, // 指向下一个 hook,形成链表结构
};

以 useState 为例,初次挂载的时候执行 mountState,更新的时候执行 updateState。

  • mountState:构建链表
  • updateState:按照顺序遍历链表,获取 数据进行页面渲染

hook 的渲染其实是根据“依次遍历”来定位每个 hook 内容,如果前后两次读到的链表在顺序上出现差异,那么渲染的结果自然是不可控的。

假设在if循环中使用 useState。

// eslint-disabled-next-line
if (Math.random() > 0.5) {
  useState('first');
}
useState(100);

如上所示例子中,假设第一次调用时 Math.random()>0.5,则底层的 _hooks 数组结构如下:

_hooks: [
  {
    memoizedState: 'first',
    baseState: 'first',
    baseQueue: null,
    queue: null,
    next: {
      memoizedState: 100,
      baseState: 100,
      baseQueue: null,
      queue: null,
    },
  }
]

假设第二次调用时 Math.random()<0.5,则会将链表中的 'first' 赋值给100,从而造成显示错误。

React Hooks 是为了简化组件逻辑和提高代码可读性而设计的。将 Hook 放在 if/循环/嵌套函数中会破坏它们的封装性和可预测性,使得代码更难维护和理解。同时,这样做也增加了代码的复杂度,可能会导致性能下降和潜在的错误。

因此,在编写 React 函数组件时,一定要遵循 Hook 规则,只在顶层使用 Hooks,并且不要在循环、条件或嵌套函数中调用。

  • 只能在函数最外层调用 Hook 。不要在循环、条件语句或子函数中调用useState、useEffect等。

  • 只能在React函数组件或者自定义 Hook 调用 Hook ,不能在其他JavaScript函数中调用。

2. 定时器hook实现

入门案例:利用 react hook 实现一个计时器

1)简单实现

import React, { useEffect, useState } from "react";

const List = () => {
  const [count,setCount] = useState(0);

  useEffect(()=>{
    const timeRef = setInterval(()=>{
      setCount((count) => count + 1);
    },1000)

    return ()=>{
      clearInterval(timeRef);
    }
  },[]);

  return (
    <div>{count}</div>
  );
};

export default List;

2)封装 hook(支持自定义初始值和时间间隔)

// 使用
const {count} = useInterval(0,1000);
// 封装hook
const useInterval = (initValue: number,delay:number) => {
  const [count, setCount] = useState(initValue);

  useEffect(() => {
    const timer = setInterval(()=>{
        setCount(count => count + 1)
    }, delay);

    return () => {
      clearInterval(timer);
    };
  }, []);

  return {count}
};

3)封装 hook(改变当前组件内状态)

// 使用setCount(count => count + 1)
useInterval(()=> setCount(count => count + 1),1000);
// 封装hook
const useInterval = (callback: ()=> void,delay:number) => {
  useEffect(()=>{
    let timer = setInterval(callback,delay);

    return ()=>{
      clearInterval(timer);
    }

  },[]);
};

// 使用setCount(count + 1)
useInterval(()=> setCount(count + 1),1000);
// 封装 hook
const useInterval = (callback: () => void, delay: number) => {
  useEffect(() => {
    const time = setInterval(callback, delay);

    return () => {
      clearInterval(time);
    };
  });
};

React Hook使用时必须显示指明依赖,不能在条件语句中声明Hook

3. 封装 Button 组件

Button 调用方式如下所示:

<Button 
    classNames='btn1 btn2' 
    onClick={()=>alret(1)} 
    size='middle'
>按钮文案</Button>

封装一个 Button 组件如下:

const Button: React.FC = memo((props:any) => {
    const { classNames, onClick, size } = props;
    const sizeList = ['small','large','middle'];

    const getClassName = ()=>{
        if(size && sizeList.includes(size)){
            return `btn-${size} ${classNames}`;
        }
        return classNames;
    }

    const handleClick = ()=>{
        if(onClick){
            onClick();
        }
    }

    return (
        <div
            className={getClassName()}
            onClick={handleClick}
        >
            {props.children}
        </div>
    );
});

.btn-small{
    width: 32px;
    height:24px;
}

.btn-middle{
    width: 50px;
    height:32px;
}

.btn-large{
    width: 88px;
    height:42px;
}

4. 封装 usePrevious Hook

const usePreValue = (value:any)=>{
    const preValue = useRef(null);
    useEffect(()=>{
        preValue.current = value;
        console.log(preValue.current, 'preValue.current');
    });
    return preValue;
}
  1. 初始渲染:
    • preValue.current 初始为 null
    • 组件渲染完成后执行 useEffect,将当前 value 赋值给 preValue.current
  2. 后续更新:
    • 组件重新渲染时,preValue.current 还保持着上一次的值
    • 渲染完成后,useEffect 再次执行,更新 preValue.current 为最新值

组件中使用:

const Case: React.FC = () => {
    const [count, setCount] = useState(0);
    const increment = () => setCount((c) => c + 1);
    const preCount = usePreValue(count);
    console.log(count, 'count', preCount, 'preCount');
    return (
        <div className="container">
            <div>Count: {count}</div>
            <div onClick={increment} >+1</div>
        </div>
    );
};

image.png

5. 封装 useDebounce Hook

image.png

点击按钮3秒后打印,只执行1次,不会重复执行。

const dbouncedFunction = useDbounce((str:string) => {
    console.log('数据保存到数据库:', str);
}, 3000);

return (
    <div onClick={() => dbouncedFunction('hello!')}>save</div>
);