✅ 无缝衔接基础版:基于「函数式组件+React Hook」核心,只讲基础版未覆盖的进阶刚需知识点 ,零基础无断层过渡
✅ 2026最新技术栈:聚焦企业100%在用的主流方案(React 18并发特性、React Router 6、Axios封装、状态管理),剔除过时内容
✅ 实战导向:所有知识点配套「企业级代码模板+真实场景案例」,复制即可复用,学完直接能对接项目开发
✅ 条理清晰:按「核心进阶Hook→路由进阶→网络请求→状态管理→性能优化→工程化」逻辑排序,符合学习认知规律
✅ 学习目标:从「会写基础React代码」升级为「能开发企业级React应用」,具备前端工程师核心竞争力
一、前置认知:React进阶的核心价值(为什么要学这些?)
基础版我们掌握了「JSX、useState、组件化、父传子」等基础能力,能写简单的静态页面和基础交互。但企业开发中,还会遇到这些核心问题:
- 组件销毁后如何清除定时器/取消请求?(需要
useEffect生命周期) - 多层嵌套组件如何高效传值?(需要
useContext跨组件通信) - 如何实现页面跳转、权限控制?(需要React Router 6进阶)
- 如何对接后端接口、统一处理请求错误?(需要Axios企业级封装)
- 复杂项目的全局数据如何管理?(需要 Zustand 状态管理)
- 项目体积大、加载慢如何优化?(需要性能优化技巧)
本进阶版就是为解决这些「企业开发刚需问题」而生,所有知识点都是前端工程师日常工作的核心内容,也是面试高频考点。
二、核心进阶Hook:从"能写"到"会写"的关键(⭐⭐⭐⭐⭐ 必学)
基础版我们只学了useState,这是React的基础数据管理Hook。进阶阶段,我们需要掌握4个核心Hook,覆盖「生命周期、跨组件通信、DOM操作、缓存优化」所有核心场景。
✔️ 1. useEffect:生命周期管理(组件的"出生、成长、死亡")
1.1 为什么需要useEffect?
基础版的代码都是"同步执行"的,但实际开发中,我们需要处理「异步操作」(如页面加载后请求接口、组件销毁前清除定时器),这些操作需要绑定到组件的特定"生命周期阶段",而useEffect就是React 18中唯一的生命周期解决方案。
1.2 核心概念:副作用
"副作用"指的是组件渲染之外的操作,比如:
- 网络请求(调用后端接口)
- 操作DOM(获取DOM元素、设置定时器/计时器)
- 订阅/取消订阅(如WebSocket连接)
useEffect的作用就是「管理这些副作用」,并控制它们的执行时机。
1.3 基础语法(固定模板,背下来)
jsx
import { useEffect } from 'react';
function MyComponent() {
// 副作用函数:要执行的异步操作/DOM操作
useEffect(() => {
// 1. 这里写要执行的副作用代码(如请求接口、设置定时器)
// 2. 清理函数(可选):组件销毁时执行,用于清除副作用
return () => {
// 如:清除定时器、取消网络请求、关闭WebSocket
};
}, [依赖数组]); // 关键:控制副作用的执行时机
return <div>组件内容</div>;
}
1.4 3种核心用法(覆盖99%场景,实战必备)
用法1:组件挂载后执行一次(最常用)
场景:页面加载后请求初始化数据(如商品列表、用户信息)、设置全局事件监听。
核心:依赖数组传「空数组[]」,表示"只在组件第一次渲染后执行一次"。
jsx
import { useState, useEffect } from 'react';
function GoodsList() {
const [goods, setGoods] = useState([]);
// 组件挂载后请求商品列表(只执行一次)
useEffect(() => {
const fetchGoods = async () => {
const res = await fetch('/api/goods'); // 模拟请求后端接口
const data = await res.json();
setGoods(data);
};
fetchGoods();
}, []); // 空依赖数组 → 只执行一次
return (
<div>
{goods.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
用法2:依赖数据变化时执行
场景:搜索框输入后触发搜索、表单值变化后做校验。
核心:依赖数组传「具体变量」,当变量的值发生变化时,副作用函数重新执行。
jsx
import { useState, useEffect } from 'react';
function SearchBox() {
const [keyword, setKeyword] = useState('');
const [results, setResults] = useState([]);
// 当keyword变化时,触发搜索(输入完成后自动查)
useEffect(() => {
const timer = setTimeout(async () => {
const res = await fetch(`/api/search?keyword=${keyword}`);
const data = await res.json();
setResults(data);
}, 500); // 防抖:输入500ms后再请求,避免频繁调用接口
// 清理函数:输入变化时,清除上一个定时器
return () => clearTimeout(timer);
}, [keyword]); // 依赖keyword → 只有keyword变了才执行
return (
<div>
<input
type="text"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
placeholder="请输入搜索关键词"
/>
<div>搜索结果:{results.map(item => <p key={item.id}>{item.name}</p>)}</div>
</div>
);
}
用法3:组件销毁时执行清理
场景:清除定时器、取消未完成的网络请求、关闭WebSocket连接。
核心:在useEffect的返回函数中写清理逻辑,组件销毁时自动执行。
jsx
import { useEffect } from 'react';
function TimerComponent() {
useEffect(() => {
// 组件挂载时设置定时器
const timer = setInterval(() => {
console.log('定时器执行中...');
}, 1000);
// 组件销毁时清除定时器(避免内存泄漏)
return () => clearInterval(timer);
}, []);
return <div>定时器组件</div>;
}
1.5 高频踩坑点(必记)
- ❌ 忘记写依赖数组:
useEffect(() => {})会导致副作用函数「每次组件渲染都执行」,引发性能问题(如频繁请求接口)。 - ❌ 依赖数组遗漏变量:使用了组件内的变量,但没写进依赖数组,会导致副作用函数中拿到的是旧值。
- ❌ 清理函数不执行:如果副作用是"持续性操作"(定时器、请求),必须写清理函数,否则会导致内存泄漏。
✔️ 2. useContext:跨组件通信(解决多层嵌套传值痛点)
2.1 为什么需要useContext?
基础版我们学了「父传子」(通过props),但如果遇到「多层嵌套组件」(如:爷爷→爸爸→儿子→孙子),要把数据从爷爷传到孙子,就需要层层传递props,这种写法叫"props透传",代码冗余且难维护。
useContext的作用就是「实现跨组件通信」:数据只需要在顶层定义,任意深层组件都能直接获取,无需层层传递。
2.2 核心原理(3步曲,固定模板)
- 创建Context:用
createContext创建一个"数据容器",用于存储要共享的数据。 - 提供数据:用
Context.Provider包裹顶层组件,通过value属性提供要共享的数据。 - 消费数据:在任意深层组件中,用
useContext获取共享的数据。
2.3 实战案例(用户信息全局共享)
场景:登录后,用户信息(用户名、头像)需要在导航栏、个人中心、订单页等多个组件中展示,用useContext实现全局共享。
jsx
// 1. 创建Context(单独文件:src/contexts/UserContext.jsx)
import { createContext, useContext, useState } from 'react';
// 创建Context,默认值可选(一般传null)
const UserContext = createContext(null);
// 定义Provider组件,提供数据和方法
export function UserProvider({ children }) {
// 要共享的全局数据(用户信息)
const [userInfo, setUserInfo] = useState(null);
// 共享的方法(登录、退出登录)
const login = (data) => {
setUserInfo(data); // 登录成功,更新用户信息
};
const logout = () => {
setUserInfo(null); // 退出登录,清空用户信息
};
// 提供数据:所有子组件都能获取到value中的内容
return (
<UserContext.Provider value={{ userInfo, login, logout }}>
{children} {/* 顶层组件的内容(如App组件) */}
</UserContext.Provider>
);
}
// 自定义Hook,简化使用(推荐写法,不用每次都写useContext)
export function useUser() {
return useContext(UserContext);
}
jsx
// 2. 顶层组件包裹(src/main.jsx)
import { createRoot } from 'react-dom/client';
import App from './App';
import { UserProvider } from './contexts/UserContext';
// 用UserProvider包裹App,使所有子组件都能访问用户信息
const root = createRoot(document.getElementById('root'));
root.render(
<UserProvider>
<App />
</UserProvider>
);
jsx
// 3. 任意组件使用共享数据(如导航栏组件)
import { useUser } from './contexts/UserContext';
function Navbar() {
// 直接获取全局用户信息,无需props传递
const { userInfo, logout } = useUser();
return (
<nav>
<div>网站Logo</div>
{userInfo ? (
<div>
<img src={userInfo.avatar} alt="用户头像" style={{ width: '40px' }} />
<span>{userInfo.name}</span>
<button onClick={logout}>退出登录</button>
</div>
) : (
<button>去登录</button>
)}
</nav>
);
}
jsx
// 4. 登录组件调用共享方法(如登录页)
import { useUser } from './contexts/UserContext';
import { useState } from 'react';
function Login() {
const { login } = useUser();
const [formData, setFormData] = useState({ username: '', password: '' });
const handleSubmit = async (e) => {
e.preventDefault();
// 调用后端登录接口
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const data = await res.json();
// 登录成功,更新全局用户信息
login(data.user);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={formData.username}
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
placeholder="用户名"
/>
<input
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
placeholder="密码"
/>
<button type="submit">登录</button>
</form>
);
}
2.4 核心注意事项
- ✅
useContext适合共享「全局通用数据」(如用户信息、主题配置、权限状态),不适合频繁变化的局部数据。 - ✅ 不要过度使用:如果只是父子组件传值,优先用props,
useContext用于解决"多层透传"问题。 - ✅ 性能优化:如果Context中的数据频繁变化,会导致所有使用该Context的组件重新渲染,后续可结合
useMemo优化(下文会讲)。
✔️ 3. useRef:DOM操作与数据持久化
3.1 核心作用(2个高频场景)
useRef有两个核心用途,都是企业开发中的刚需:
- 直接操作DOM元素(如获取输入框焦点、获取DOM尺寸);
- 持久化存储数据(组件重新渲染时,数据不会丢失,且修改不会触发重新渲染)。
3.2 用法1:操作DOM元素(最常用)
场景:登录页点击"登录"后,若用户名未输入,自动聚焦到用户名输入框;获取轮播图DOM元素实现滚动。
jsx
import { useRef } from 'react';
function LoginForm() {
// 1. 创建ref对象,绑定到DOM元素
const usernameRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const username = usernameRef.current.value; // 2. 通过ref获取DOM值
if (!username) {
// 3. 操作DOM:自动聚焦到输入框
usernameRef.current.focus();
alert('请输入用户名');
return;
}
// 后续登录逻辑...
};
return (
<form onSubmit={handleSubmit}>
{/* 绑定ref:ref={usernameRef} */}
<input
type="text"
ref={usernameRef}
placeholder="请输入用户名"
/>
<input
type="password"
placeholder="请输入密码"
/>
<button type="submit">登录</button>
</form>
);
}
3.3 用法2:持久化存储数据
场景:存储定时器ID、记录组件渲染次数、存储上一次的状态值。
核心优势:修改ref中的数据(ref.current = 新值)不会触发组件重新渲染,适合存储"不影响UI的数据"。
jsx
import { useRef, useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
// 持久化存储渲染次数(修改不会触发渲染)
const renderCountRef = useRef(0);
// 组件每次渲染,渲染次数+1
useEffect(() => {
renderCountRef.current += 1;
console.log('组件渲染次数:', renderCountRef.current);
});
return (
<div>
<p>当前计数:{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<p>组件已渲染:{renderCountRef.current}次</p>
</div>
);
}
✔️ 4. useMemo & useCallback:性能优化Hook(避免不必要的重渲染)
4.1 为什么需要性能优化?
React中,当父组件的状态变化时,会重新渲染自身以及所有子组件。如果子组件比较复杂(如长列表、复杂表单),频繁的重渲染会导致页面卡顿。
useMemo和useCallback的核心作用是「缓存数据/函数」,避免不必要的重渲染,提升应用性能。
4.2 useMemo:缓存计算结果
场景:复杂的计算逻辑(如长列表过滤、数据排序),避免每次渲染都重复计算。
jsx
import { useState, useMemo } from 'react';
function FilterList() {
const [list, setList] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9]);
const [keyword, setKeyword] = useState('');
// 复杂计算:过滤列表(模拟大量数据计算)
// useMemo缓存计算结果,只有keyword变化时才重新计算
const filteredList = useMemo(() => {
console.log('执行过滤计算...');
return list.filter(item => item.toString().includes(keyword));
}, [list, keyword]); // 依赖数组:只有list或keyword变化时,重新计算
return (
<div>
<input
type="text"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
placeholder="输入数字过滤"
/>
<ul>
{filteredList.map(item => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
- 没有
useMemo:每次组件渲染(如输入框输入)都会执行过滤计算,即使list和keyword都没变化; - 有
useMemo:只有依赖数组中的list或keyword变化时,才重新执行过滤计算,减少性能损耗。
4.3 useCallback:缓存函数
场景:向子组件传递函数时,避免每次渲染都创建新的函数实例,导致子组件不必要的重渲染。
jsx
import { useState, useCallback, memo } from 'react';
// 子组件:用memo包裹,避免不必要的重渲染
const Child = memo(({ onBtnClick }) => {
console.log('子组件渲染了');
return <button onClick={onBtnClick}>子组件按钮</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// useCallback缓存函数,只有count变化时才创建新函数
const handleBtnClick = useCallback(() => {
console.log('点击了子组件按钮,count:', count);
}, [count]); // 依赖count:只有count变化时,才重新创建函数
return (
<div>
<p>父组件计数:{count}</p>
<button onClick={() => setCount(count + 1)}>父组件+1</button>
{/* 传递缓存的函数给子组件 */}
<Child onBtnClick={handleBtnClick} />
</div>
);
}
- 没有
useCallback:每次父组件渲染(点击+1)都会创建新的handleBtnClick函数,导致子组件重新渲染; - 有
useCallback:只有依赖数组中的count变化时,才创建新函数,子组件不会因为父组件无关状态变化而重渲染。
4.4 核心注意事项
- ❌ 不要过度优化:简单的计算或函数不需要用
useMemo/useCallback,缓存本身也有性能开销,只在"确实出现性能问题"或"子组件复杂"时使用; - ✅
memo配合使用:useCallback通常需要和memo(包裹子组件)配合使用,否则无法达到优化效果; - ✅ 依赖数组正确:
useMemo/useCallback的依赖数组要包含所有用到的变量,否则会拿到旧值。
三、React Router 6 进阶:实现多页面应用与权限控制(⭐⭐⭐⭐⭐ 必学)
基础版我们没涉及路由,而企业开发的React应用都是「单页应用(SPA)」,需要通过路由实现"页面跳转"功能。React Router 6是2026年的主流版本,进阶部分我们需要掌握「路由懒加载、嵌套路由、路由守卫、路由传参」等企业级用法。
✔️ 1. 先回顾:React Router 6 基础配置(衔接基础)
首先确保已安装依赖:
bash
npm install react-router-dom@6
基础路由配置(src/App.jsx):
jsx
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import Home from './views/Home';
import Login from './views/Login';
import About from './views/About';
function App() {
return (
<Router>
{/* 导航栏:页面跳转链接 */}
<nav>
<Link to="/">首页</Link>
<Link to="/login">登录</Link>
<Link to="/about">关于我们</Link>
</nav>
{/* 路由匹配:路径对应组件 */}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/about" element={<About />} />
{/* 404页面:匹配所有未定义的路径 */}
<Route path="*" element={<div>404 页面未找到</div>} />
</Routes>
</Router>
);
}
export default App;
✔️ 2. 进阶核心1:路由懒加载(性能优化TOP1,企业必做)
2.1 核心问题
基础配置中,所有页面组件(Home、Login、About)会在应用初始化时一次性加载,导致首屏加载时间长、性能差。尤其是中大型项目,页面越多,首屏加载越慢。
2.2 路由懒加载原理
通过React.lazy和Suspense配合,实现「页面组件按需加载」:只有当用户访问某个路由时,才加载对应的组件资源,减少首屏加载体积,提升加载速度。
2.3 企业级实现代码(复制即用)
jsx
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import { lazy, Suspense } from 'react';
// 路由懒加载:访问时才加载组件(替代直接import)
const Home = lazy(() => import('./views/Home'));
const Login = lazy(() => import('./views/Login'));
const About = lazy(() => import('./views/About'));
function App() {
return (
<Router>
<nav>
<Link to="/">首页</Link>
<Link to="/login">登录</Link>
<Link to="/about">关于我们</Link>
</nav>
{/* Suspense:加载组件时显示占位内容(避免页面空白) */}
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<div>404 页面未找到</div>} />
</Routes>
</Suspense>
</Router>
);
}
lazy(() => import('组件路径')):定义懒加载组件;Suspense:包裹所有懒加载路由,fallback属性指定加载时的占位内容(如"加载中..."、加载动画);- 效果:首屏只加载必要的导航栏和Suspense内容,访问对应路由时才加载该页面组件,首屏加载速度提升80%。
✔️ 3. 进阶核心2:嵌套路由(页面布局复用,企业规范)
3.1 应用场景
企业开发中,很多页面会共享相同的布局(如后台管理系统的"侧边栏+顶部导航"布局),嵌套路由可以实现「布局复用」,避免重复写布局代码。
3.2 核心概念
- 父路由:包含公共布局的路由(如
/dashboard); - 子路由:嵌套在父路由中的路由(如
/dashboard/user、/dashboard/goods); Outlet:父路由组件中,用于渲染子路由内容的"占位符"。
3.3 企业级实现代码(后台管理系统案例)
jsx
import { BrowserRouter as Router, Routes, Route, Link, Outlet } from 'react-router-dom';
import { lazy, Suspense } from 'react';
// 懒加载组件
const Dashboard = lazy(() => import('./views/Dashboard'));
const UserList = lazy(() => import('./views/UserList'));
const GoodsList = lazy(() => import('./views/GoodsList'));
const Login = lazy(() => import('./views/Login'));
// 父路由组件:包含公共布局(侧边栏+顶部导航)
function DashboardLayout() {
return (
<div style={{ display: 'flex' }}>
{/* 侧边栏(公共部分) */}
<aside style={{ width: '200px', borderRight: '1px solid #ccc', padding: '20px' }}>
<Link to="/dashboard/user" style={{ display: 'block', margin: '10px 0' }}>用户管理</Link>
<Link to="/dashboard/goods" style={{ display: 'block', margin: '10px 0' }}>商品管理</Link>
</aside>
{/* 主内容区:子路由内容会渲染在这里 */}
<main style={{ flex: 1, padding: '20px' }}>
<Outlet /> {/* 子路由占位符 */}
</main>
</div>
);
}
function App() {
return (
<Router>
<Suspense fallback={<div>加载中...</div>}>
<Routes>
{/* 登录页(无布局) */}
<Route path="/login" element={<Login />} />
{/* 嵌套路由:父路由渲染布局,子路由渲染具体内容 */}
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<div>请选择管理模块</div>} /> {/* 默认子路由(访问/dashboard时显示) */}
<Route path="user" element={<UserList />} /> {/* /dashboard/user */}
<Route path="goods" element={<GoodsList />} /> {/* /dashboard/goods */}
</Route>
<Route path="*" element={<div>404 页面未找到</div>} />
</Routes>
</Suspense>
</Router>
);
}
- 效果:访问
/dashboard/user或/dashboard/goods时,都会显示"侧边栏+顶部导航"布局,只有主内容区显示不同的子组件,实现布局复用; - 优势:修改布局时,只需修改父路由组件,所有子路由页面都会同步更新,维护成本极低。
✔️ 4. 进阶核心3:路由守卫(权限控制,企业刚需)
4.1 应用场景
企业应用中,有些页面需要「登录后才能访问」(如个人中心、后台管理页),未登录用户访问时,需要自动跳转到登录页。路由守卫就是实现这一功能的核心方案。
4.2 实现原理
通过「自定义鉴权组件」包裹需要权限的路由,在组件中判断用户是否登录(通过useUser获取全局用户信息),未登录则重定向到登录页,已登录则显示页面内容。
4.3 企业级实现代码(结合useContext)
jsx
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { lazy, Suspense } from 'react';
import { useUser } from './contexts/UserContext';
// 懒加载组件
const Home = lazy(() => import('./views/Home'));
const Login = lazy(() => import('./views/Login'));
const Dashboard = lazy(() => import('./views/Dashboard'));
const UserList = lazy(() => import('./views/UserList'));
// 1. 自定义鉴权组件(路由守卫核心)
function ProtectedRoute({ children }) {
const { userInfo } = useUser(); // 获取全局用户信息
if (!userInfo) {
// 未登录:重定向到登录页,记录当前路径(登录后可跳回)
return <Navigate to="/login" replace state={{ from: window.location.pathname }} />;
}
// 已登录:显示页面内容
return children;
}
function App() {
return (
<Router>
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
{/* 需要权限的路由:用ProtectedRoute包裹 */}
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
>
<Route index element={<div>请选择管理模块</div>} />
<Route path="user" element={<UserList />} />
</Route>
<Route path="*" element={<div>404 页面未找到</div>} />
</Routes>
</Suspense>
</Router>
);
}
jsx
// 登录组件优化:登录成功后跳回之前访问的页面
import { useUser } from './contexts/UserContext';
import { useState, useLocation, Navigate } from 'react-router-dom';
function Login() {
const { login, userInfo } = useUser();
const [formData, setFormData] = useState({ username: '', password: '' });
const location = useLocation(); // 获取当前路由信息
// 从state中获取之前访问的路径(未登录时被拦截的路径)
const fromPath = location.state?.from || '/';
// 已登录:直接跳回之前的页面
if (userInfo) {
return <Navigate to={fromPath} replace />;
}
const handleSubmit = async (e) => {
e.preventDefault();
// 调用后端登录接口
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const data = await res.json();
login(data.user); // 更新全局用户信息
// 登录成功,跳回之前访问的页面
<Navigate to={fromPath} replace />;
};
// 表单内容(和之前一致)
return <form onSubmit={handleSubmit}>...</form>;
}
- 核心逻辑:未登录用户访问
/dashboard时,被ProtectedRoute拦截,重定向到登录页,并记录当前路径;登录成功后,自动跳回之前想访问的页面,用户体验极佳; - 拓展:可以基于此实现「角色权限控制」(如管理员才能访问
/dashboard/admin,普通用户无权限),只需在ProtectedRoute中增加角色判断逻辑。
✔️ 5. 进阶核心4:路由传参(页面间数据传递)
5.1 2种核心传参方式(覆盖所有场景)
开发中经常需要"页面跳转时传递数据"(如点击商品列表跳转到详情页,传递商品ID),React Router 6提供2种主流传参方式:
方式1:search参数(地址栏可见,推荐)
- 特点:参数拼接在URL后面(如
/detail?id=1&name=React),刷新页面参数不丢失; - 适用场景:传递简单、非敏感数据(如ID、页码、搜索关键词)。
jsx
// 列表页:跳转并传参
import { Link } from 'react-router-dom';
function GoodsList() {
const goods = [{ id: 1, name: 'React实战教程' }, { id: 2, name: 'Vue3基础教程' }];
return (
<ul>
{goods.map(item => (
// 传参:通过search拼接参数
<li key={item.id}>
<Link to={`/detail?id=${item.id}&name=${item.name}`}>{item.name}</Link>
</li>
))}
</ul>
);
}
// 详情页:接收参数
import { useSearchParams } from 'react-router-dom';
function GoodsDetail() {
// useSearchParams:获取search参数(类似URLSearchParams)
const [searchParams] = useSearchParams();
const id = searchParams.get('id'); // 获取id参数
const name = searchParams.get('name'); // 获取name参数
return (
<div>
<h3>商品详情</h3>
<p>商品ID:{id}</p>
<p>商品名称:{name}</p>
</div>
);
}
方式2:state参数(地址栏不可见)
- 特点:参数存储在路由状态中,不显示在URL上,刷新页面参数不丢失;
- 适用场景:传递敏感数据(如用户信息)或复杂对象。
jsx
// 列表页:跳转并传参
import { Link } from 'react-router-dom';
function GoodsList() {
const goods = [{ id: 1, name: 'React实战教程', price: 99 }];
return (
<ul>
{goods.map(item => (
<li key={item.id}>
{/* 传参:通过state传递复杂数据 */}
<Link to="/detail" state={{ goodsInfo: item }}>
{item.name}
</Link>
</li>
))}
</ul>
);
}
// 详情页:接收参数
import { useLocation } from 'react-router-dom';
function GoodsDetail() {
const location = useLocation();
const { goodsInfo } = location.state || {}; // 获取state参数
return (
<div>
<h3>商品详情</h3>
<p>商品ID:{goodsInfo?.id}</p>
<p>商品名称:{goodsInfo?.name}</p>
<p>商品价格:{goodsInfo?.price}</p>
</div>
);
}
四、网络请求进阶:Axios 企业级封装(前后端交互核心)
基础版我们没涉及网络请求,而企业开发的核心是"前端对接后端接口"。Axios是React项目中100%在用的网络请求库,进阶阶段我们需要掌握「企业级Axios封装」,实现请求统一配置、拦截器、错误处理等核心功能。
✔️ 1. 为什么需要封装Axios?
直接使用Axios发起请求,会存在这些问题:
- 重复代码:每个请求都要写
baseURL、headers,冗余且难维护; - 错误处理:每个请求都要写
try/catch,代码繁琐; - 权限统一:所有请求都要添加Token(用户登录凭证),分散处理易遗漏;
- 响应处理:后端返回数据格式统一(如
{ code: 200, msg: '', data: {} }),需要统一解析; - 异常处理:Token过期、网络错误、接口报错等,需要统一提示用户。
企业级封装就是为了解决这些问题,实现「一次封装,全项目复用」,提升开发效率和代码可维护性。
✔️ 2. 步骤1:安装Axios
bash
npm install axios
✔️ 3. 步骤2:企业级Axios封装(核心代码,复制即用)
在src/utils/request.js中创建封装文件:
jsx
// src/utils/request.js
import axios from 'axios';
import { ElMessage } from 'element-plus'; // 可选:UI组件库的提示组件(需安装element-plus)
import { useUser } from '../contexts/UserContext';
// 1. 创建Axios实例,配置基础参数
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 接口根路径(从环境变量获取,推荐)
timeout: 10000, // 请求超时时间(10秒)
headers: {
'Content-Type': 'application/json;charset=utf-8' // 默认请求头
}
});
// 2. 请求拦截器:请求发送前执行(统一添加Token、处理参数)
request.interceptors.request.use(
(config) => {
// 获取全局用户信息(Token)
const { userInfo } = useUser();
// 统一添加Token(后端鉴权用)
if (userInfo?.token) {
config.headers.Authorization = `Bearer ${userInfo.token}`;
}
return config;
},
(error) => {
// 请求发送失败(如网络错误)
ElMessage.error('请求发送失败,请稍后重试');
return Promise.reject(error);
}
);
// 3. 响应拦截器:接口返回后执行(统一解析数据、处理错误)
request.interceptors.response.use(
(response) => {
// 后端约定:code=200表示请求成功,其他为失败
const res = response.data;
if (res.code !== 200) {
// 接口报错:统一提示错误信息
ElMessage.error(res.msg || '请求失败');
return Promise.reject(res); // 抛出错误,让组件可以捕获处理
}
// 请求成功:只返回data部分(简化组件中获取数据的逻辑)
return res.data;
},
(error) => {
// 统一处理网络错误、Token过期等异常
let errorMsg = '网络异常,请稍后重试';
// Token过期(后端返回401状态码)
if (error.response?.status === 401) {
errorMsg = '登录已过期,请重新登录';
const { logout } = useUser();
logout(); // 清空用户信息
// 跳转到登录页
window.location.href = '/login';
}
ElMessage.error(errorMsg);
return Promise.reject(error);
}
);
// 4. 封装常用请求方法(get/post/put/delete),简化组件调用
export const api = {
// get请求:参数通过params传递
get(url, params) {
return request({ method: 'GET', url, params });
},
// post请求:参数通过data传递(JSON格式)
post(url, data) {
return request({ method: 'POST', url, data });
},
// put请求:更新数据
put(url, data) {
return request({ method: 'PUT', url, data });
},
// delete请求:删除数据
delete(url, params) {
return request({ method: 'DELETE', url, params });
}
};
export default request;
✔️ 4. 步骤3:创建环境变量文件(区分开发/生产环境)
在项目根目录创建.env.development(开发环境)和.env.production(生产环境)文件,用于配置不同环境的接口根路径:
env
# .env.development(开发环境)
VITE_API_BASE_URL = 'http://localhost:3000/api' # 本地后端接口地址
env
# .env.production(生产环境)
VITE_API_BASE_URL = 'https://api.your-project.com/api' # 线上后端接口地址
- 优势:打包时自动切换环境,无需手动修改代码,避免线上环境调用本地接口的错误。
✔️ 5. 步骤4:组件中使用封装后的Axios(极简)
在组件中直接导入封装好的api对象,调用对应方法即可,无需重复配置:
jsx
import { useState, useEffect } from 'react';
import { api } from '../utils/request';
function GoodsList() {
const [goods, setGoods] = useState([]);
// 页面加载后请求商品列表
useEffect(() => {
const fetchGoods = async () => {
try {
// 调用get请求,参数通过params传递
const data = await api.get('/goods/list', { page: 1, size: 10 });
setGoods(data);
} catch (error) {
console.log('请求商品列表失败:', error);
// 错误已在拦截器中统一提示,组件可根据需要额外处理
}
};
fetchGoods();
}, []);
// 新增商品(post请求)
const addGoods = async (newGoods) => {
try {
await api.post('/goods/add', newGoods);
ElMessage.success('新增商品成功');
// 重新请求列表,更新数据
fetchGoods();
} catch (error) {
console.log('新增商品失败:', error);
}
};
return (
<div>
<h3>商品列表</h3>
<ul>
{goods.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<button onClick={() => addGoods({ name: '新商品', price: 199 })}>新增商品</button>
</div>
);
}
- 优势:代码极简,无需写
try/catch(拦截器已统一处理错误),无需重复配置baseURL和headers,开发效率提升80%。
五、状态管理进阶:Zustand(2026主流,替代Redux)
基础版我们用useState管理组件内数据,用useContext管理简单全局数据。但复杂项目中(如电商购物车、多页面共享数据),useContext会存在性能问题,此时需要专业的状态管理库。
2026年,Zustand已取代Redux成为React生态的主流状态管理方案,原因是:语法简洁、无冗余代码、性能优异、学习成本低。
✔️ 1. 为什么选择Zustand?
对比Redux和useContext,Zustand的核心优势:
- 语法简单:无需写
Provider、reducer、action等冗余代码,几行代码就能实现全局状态管理; - 性能优异:自动优化重渲染,只有使用到状态的组件才会重新渲染;
- 支持中间件:可轻松集成持久化、日志等功能;
- 学习成本低:基于Hook,零基础容易上手。
✔️ 2. 步骤1:安装Zustand
bash
npm install zustand
✔️ 3. 步骤2:创建全局状态(购物车案例,企业级)
在src/stores/cartStore.js中创建状态管理文件:
jsx
// src/stores/cartStore.js
import { create } from 'zustand';
import { persist } from 'zustand/middleware'; // 持久化中间件(可选,用于刷新页面数据不丢失)
// 创建购物车状态管理
const useCartStore = create(
// persist:持久化存储(默认存储到localStorage),刷新页面购物车数据不丢失
persist(
(set, get) => ({
// 1. 全局状态:购物车列表
cartList: [],
// 2. 状态操作方法:添加商品到购物车
addToCart: (goods) => {
const currentCart = get().cartList;
// 判断商品是否已在购物车中
const isExist = currentCart.find(item => item.id === goods.id);
if (isExist) {
// 已存在:数量+1
set({
cartList: currentCart.map(item =>
item.id === goods.id ? { ...item, count: item.count + 1 } : item
)
});
} else {
// 不存在:添加新商品,数量设为1
set({
cartList: [...currentCart, { ...goods, count: 1 }]
});
}
},
// 3. 状态操作方法:从购物车删除商品
removeFromCart: (goodsId) => {
const currentCart = get().cartList;
set({
cartList: currentCart.filter(item => item.id !== goodsId)
});
},
// 4. 状态操作方法:清空购物车
clearCart: () => {
set({ cartList: [] });
},
// 5. 计算属性:购物车商品总数
getCartTotalCount: () => {
return get().cartList.reduce((total, item) => total + item.count, 0);
},
// 6. 计算属性:购物车商品总价
getCartTotalPrice: () => {
return get().cartList.reduce((total, item) => total + item.price * item.count, 0);
}
}),
{
name: 'cart-storage', // 持久化存储的key(localStorage中的键名)
}
)
);
export default useCartStore;
✔️ 4. 步骤3:组件中使用全局状态(任意组件均可访问)
场景1:商品列表组件(添加商品到购物车)
jsx
import { useState, useEffect } from 'react';
import { api } from '../utils/request';
import useCartStore from '../stores/cartStore';
function GoodsList() {
const [goods, setGoods] = useState([]);
// 从全局状态中获取addToCart方法
const addToCart = useCartStore(state => state.addToCart);
// 请求商品列表
useEffect(() => {
const fetchGoods = async () => {
const data = await api.get('/goods/list');
setGoods(data);
};
fetchGoods();
}, []);
return (
<div>
<h3>商品列表</h3>
<div style={{ display: 'flex', gap: '20px', flexWrap: 'wrap' }}>
{goods.map(item => (
<div key={item.id} style={{ border: '1px solid #ccc', padding: '10px' }}>
<p>商品名称:{item.name}</p>
<p>商品价格:¥{item.price}</p>
<button onClick={() => addToCart(item)}>加入购物车</button>
</div>
))}
</div>
</div>
);
}
场景2:购物车组件(展示购物车数据、删除商品)
jsx
import useCartStore from '../stores/cartStore';
function Cart() {
// 从全局状态中获取购物车数据和操作方法
const cartList = useCartStore(state => state.cartList);
const removeFromCart = useCartStore(state => state.removeFromCart);
const clearCart = useCartStore(state => state.clearCart);
const totalCount = useCartStore(state => state.getCartTotalCount());
const totalPrice = useCartStore(state => state.getCartTotalPrice());
return (
<div>
<h3>购物车</h3>
{cartList.length === 0 ? (
<p>购物车为空,请添加商品</p>
) : (
<>
<ul>
{cartList.map(item => (
<li key={item.id} style={{ margin: '10px 0' }}>
<span>{item.name} - ¥{item.price} x {item.count}</span>
<button onClick={() => removeFromCart(item.id)} style={{ marginLeft: '10px' }}>删除</button>
</li>
))}
</ul>
<div>
<p>商品总数:{totalCount}</p>
<p>商品总价:¥{totalPrice}</p>
<button onClick={clearCart}>清空购物车</button>
<button style={{ marginLeft: '10px' }}>去结算</button>
</div>
</>
)}
</div>
);
}
场景3:导航栏组件(展示购物车商品数量)
jsx
import { Link } from 'react-router-dom';
import useCartStore from '../stores/cartStore';
function Navbar() {
// 从全局状态中获取购物车商品总数
const totalCount = useCartStore(state => state.getCartTotalCount());
return (
<nav style={{ display: 'flex', gap: '20px', padding: '20px', borderBottom: '1px solid #ccc' }}>
<Link to="/">首页</Link>
<Link to="/goods">商品列表</Link>
<Link to="/cart">
购物车
{/* 购物车数量徽章 */}
<span style={{
background: 'red', color: 'white', borderRadius: '50%',
width: '20px', height: '20px', display: 'inline-flex',
alignItems: 'center', justifyContent: 'center', fontSize: '12px'
}}>
{totalCount}
</span>
</Link>
</nav>
);
}
- 核心优势:任意组件都能直接获取/修改全局购物车数据,无需通过props传递;刷新页面后,购物车数据不会丢失(因为使用了
persist持久化中间件);只有使用到状态的组件才会重新渲染,性能优异。
六、React 18 并发特性:提升用户体验(2026最新)
React 18 引入了「并发渲染(Concurrent Rendering)」特性,通过优化渲染优先级,让应用在处理大量数据或复杂计算时,依然保持流畅的用户交互。这是2026年企业级React应用的重要优化方向,零基础只需掌握核心用法即可。
✔️ 1. 核心问题:同步渲染的痛点
在React 18之前,渲染是「同步且不可中断」的。如果组件树庞大或有复杂计算,会阻塞主线程,导致用户交互(如点击、输入)卡顿。例如:在搜索框输入时,后台正在处理大量数据,输入内容会延迟显示,用户体验差。
✔️ 2. 核心解决方案:startTransition(标记非紧急更新)
startTransition 是React 18提供的核心API,用于标记「非紧急更新」,让React优先处理用户交互等紧急任务,延迟处理非紧急更新,避免界面卡顿。
2.1 实战案例:搜索框优化
jsx
import { useState, startTransition } from 'react';
import { api } from '../utils/request';
function SearchBox() {
const [inputValue, setInputValue] = useState('');
const [searchResults, setSearchResults] = useState([]);
const handleInputChange = (e) => {
const value = e.target.value;
// 1. 紧急更新:更新输入框值(优先处理,确保用户输入即时反馈)
setInputValue(value);
// 2. 非紧急更新:搜索数据(标记为过渡更新,延迟处理)
startTransition(async () => {
// 模拟复杂搜索或大量数据处理
const data = await api.get('/goods/search', { keyword: value });
setSearchResults(data);
});
};
return (
<div>
<input
type="text"
value={inputValue}
onChange={handleInputChange}
placeholder="输入关键词搜索商品..."
/>
<div style={{ marginTop: '10px' }}>
{searchResults.map(item => (
<p key={item.id}>{item.name}</p>
))}
</div>
</div>