一、Redux 完整指南
🎯 什么是Redux?为什么需要它?
Redux 是一个用于JavaScript应用的状态容器,提供可预测化的状态管理。它基于Flux架构,但更简洁、更强大。
// Redux的核心思想:整个应用的状态存储在一个对象树中
console.log(store.getState()) // { user: {...}, posts: [...], ui: {...} }
为什么要用Redux?🤔
前端复杂性的根本原因是大量无规律的交互和异步操作。随着应用规模增大,我们会面临:
- 组件间通信混乱:父子、兄弟、跨层级通信错综复杂
- 状态变化难以追踪:不知道状态何时、为何、如何变化
- 调试困难: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的核心原则是什么?
- 单一数据源:整个应用只有唯一Store
- State只读:唯一改变state的方式是触发action
- 使用纯函数修改:reducer必须是纯函数,接收旧state和action,返回新state
Q2:Redux和Flux的区别?
| 维度 | Flux | Redux |
|---|---|---|
| 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的优势?
- 减少样板代码:
createSlice合并action/reducer - 自动处理不可变更新:Immer集成,可直接写"可变"语法
- 内置异步支持:
createAsyncThunk简化异步action - 默认最佳实践:自动配置中间件、DevTools
- 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>
核心区别:
| 特性 | BrowserRouter | HashRouter |
|---|---|---|
| URL格式 | 标准路径 /users | 带#号 /#/users |
| 原理 | history.pushState + popstate | window.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库实现,核心原理是:
-
两种模式:
BrowserRouter:利用HTML5 History API(pushState、replaceState、popstate事件)HashRouter:利用URL的hash部分和hashchange事件
-
工作流程:
- 点击
Link组件,阻止a标签默认行为 - 通过history API更新URL
- 触发监听事件,Router组件感知变化
- 根据新的location匹配路由规则
- 渲染对应的组件
- 点击
Q2:Link和a标签的区别?
| 维度 | Link | a标签 |
|---|---|---|
| 页面刷新 | 无刷新切换 | 会刷新页面 |
| 原理 | 阻止默认事件 + 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>
} />
📊 版本对比速查表
| 特性 | v5 | v6 |
|---|---|---|
| 核心组件 | Switch | Routes |
| 路由渲染 | component/render | element |
| 精确匹配 | exact属性 | 自动精确 |
| 重定向 | Redirect | Navigate |
| 嵌套路由 | 组件内配置 | 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;
}
- 初始渲染:
- preValue.current 初始为 null
- 组件渲染完成后执行 useEffect,将当前 value 赋值给 preValue.current
- 后续更新:
- 组件重新渲染时,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>
);
};
5. 封装 useDebounce Hook
点击按钮3秒后打印,只执行1次,不会重复执行。
const dbouncedFunction = useDbounce((str:string) => {
console.log('数据保存到数据库:', str);
}, 3000);
return (
<div onClick={() => dbouncedFunction('hello!')}>save</div>
);