第一章:React 基础与开发环境
1.1 什么是 React
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| React | 一个用于构建用户界面的 JavaScript 库,由 Facebook(现 Meta)开发并开源。React 专注于应用的视图层(View),采用组件化架构,使开发者能够高效地构建可复用的 UI 组件。 | React 不是框架(如 Angular),它只负责 UI 渲染,其他功能(如路由、状态管理)需依赖生态库。 |
| 用户界面(UI) | 指用户与应用程序交互的视觉部分,如按钮、表单、列表等。React 的核心任务是高效地渲染和更新这些 UI 元素。 | React 可用于 Web(React DOM)、移动端(React Native)、桌面端(Electron)等多种平台。 |
| 声明式编程 | React 采用声明式范式:开发者描述”UI 应该是什么样”,而非”如何一步步构建 UI”。React 负责将状态变化自动映射为 DOM 更新。 | 相比命令式编程(如直接操作 DOM),声明式更易读、易维护,减少出错概率。 |
1.2 React 的核心特点
| 特点 | 说明 | 注意事项 |
|---|---|---|
| 声明式(Declarative) | 开发者通过 JSX 描述 UI 在不同状态下的样子,React 自动处理 DOM 更新。例如:<Button disabled={isLoading} />,React 根据 isLoading 值决定是否禁用按钮。 | 减少手动 DOM 操作,提升开发效率和代码可维护性。 |
| 组件化(Component-Based) | UI 被拆分为独立、可复用的组件。每个组件管理自己的状态和逻辑,可嵌套组合形成复杂界面。例如:Header、Sidebar、Comment 都是组件。 | 遵循单一职责原则,组件应尽量小而专注,便于测试和复用。 |
| 虚拟 DOM(Virtual DOM) | React 在内存中维护一个轻量级的 DOM 表示(即虚拟 DOM)。当状态变化时,React 先在虚拟 DOM 上计算差异(diffing),再批量更新真实 DOM,提升性能。 | 虚拟 DOM 不是直接操作真实 DOM,而是通过高效的 diff 算法最小化 DOM 操作次数。 |
| 单向数据流(Unidirectional Data Flow) | 数据从父组件通过 props 单向传递给子组件。子组件不能直接修改 props,只能通过回调函数通知父组件更新状态。 | 保证数据流动清晰,便于调试和追踪状态变化。避免数据”双向绑定”带来的复杂性。 |
1.3 React 生态概览
| 生态工具 | 说明 | 注意事项 |
|---|---|---|
| React DOM | 用于在 Web 浏览器中渲染 React 组件。提供 ReactDOM.render() 方法将 React 元素挂载到 DOM 节点。 | 现代 React 使用 createRoot(React 18+)替代 render,支持并发渲染。 |
| React Native | 使用 React 构建原生移动应用(iOS 和 Android)。使用原生组件而非 Web 标签,实现接近原生性能的体验。 | 学习一次,可跨平台开发,但需了解平台特定 API 和样式差异。 |
| Next.js | React 的服务端渲染(SSR)和静态生成(SSG)框架,支持 SEO 友好、高性能的 Web 应用。内置路由、API 路由、图像优化等功能。 | 适合需要 SEO 或高性能加载的项目,如官网、博客、电商平台。 |
| Remix | 全栈 Web 框架,强调 Web 原生体验和快速加载。基于 React Router,支持数据加载、表单处理等。 | 更适合复杂的数据驱动应用,学习曲线略高于 Next.js。 |
| Gatsby | 基于 React 的静态站点生成器,适合构建文档、博客、营销页面等。从多种数据源预生成静态页面。 | 构建速度快,部署简单,适合内容为主的网站。 |
| Redux / Zustand | 状态管理库。Redux 用于大型应用的全局状态管理;Zustand 是轻量级替代方案,API 更简洁。 | 小型项目可直接使用 useState 和 useContext,避免过度设计。 |
1.4 使用 Create React App 搭建项目
| 方法/命令 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
create-react-app | npx create-react-app <project-name> | 创建一个新的 React 项目,包含默认配置、开发服务器、构建脚本等。 | npx create-react-app my-app | 推荐使用 npx,无需全局安装。<project-name> 不能包含大写字母或特殊字符。 |
--template | npx create-react-app <project-name> --template [template-name] | 使用指定模板创建项目,如 TypeScript、Redux 等。 | npx create-react-app my-app --template typescript | 常用模板:typescript、cra-template-redux、cra-template-typescript-redux。 |
1.5 项目结构解析与开发服务器启动
| 文件/目录 | 说明 | 注意事项 |
|---|---|---|
public/index.html | 应用的 HTML 入口文件。React 组件将挂载到 <div id="root"></div>。 | 可在此添加 meta 标签、引入 CDN 资源等。 |
src/index.js | JavaScript 入口文件。调用 ReactDOM.createRoot 并渲染根组件 <App /> 到 DOM。 | React 18 使用 createRoot 启用并发模式。 |
src/App.js | 根组件,初始 UI 内容。可被修改或替换。 | 通常作为其他组件的容器。 |
src/components/ | (建议)存放可复用的 UI 组件。CRA 不自动生成,需手动创建。 | 良好的项目结构应分离组件、页面、工具等。 |
package.json | 项目配置文件,包含依赖、脚本、版本等信息。 | 所有 npm 脚本在此定义。 |
node_modules/ | 存放项目依赖包。由 npm install 自动生成。 | 不应提交到版本控制(.gitignore 已包含)。 |
package-lock.json | 锁定依赖版本,确保安装一致性。 | 应提交到 Git,保证团队环境一致。 |
| npm 脚本 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
npm start | npm run start | 启动开发服务器,默认在 http://localhost:3000 打开应用。 | npm start | 开发服务器支持热重载(HMR),修改代码后自动刷新。 |
npm run build | npm run build | 构建生产环境优化的静态文件,输出到 build/ 目录。 | npm run build | 用于部署,文件已压缩、优化。 |
npm test | npm run test | 启动测试运行器,执行项目中的测试文件。 | npm test | CRA 使用 Jest + React Testing Library。 |
npm run eject | npm run eject | 将所有配置暴露出来,不再受 CRA 封装限制。 | npm run eject | ⚠️ 不可逆操作!仅在需要深度定制配置时使用。 |
1.6 最小可用实现 ToDoList
| 文件路径 | 主要用途 | 用到的主要接口/语法 |
|---|---|---|
public/index.html | HTML 页面模板,React 应用挂载点 | <div id="root"></div>(React 挂载点) |
src/index.js | 应用入口文件,启动 React 并渲染根组件 | ReactDOM.createRoot(document.getElementById('root')).render(<BrowserRouter><App /></BrowserRouter>); |
src/App.js | 主应用组件,定义路由和整体布局 | 状态管理,内部的标签、组件、值都会作为参数,传递给 Provider 定义时的函数(一般在 context 目录下定义) |
src/App.css | 全局样式或 App 组件专用样式 | CSS 样式规则、类选择器 |
src/context/TodoContext.js | 管理全局待办事项状态 | useContext + useReducer 在组件间完成状态共享 |
src/pages/AllTodos.js | 显示所有待办事项的页面 | 组件组合,解析数据,渲染到页面 |
src/pages/ActiveTodos.js | 显示未完成的待办事项的页面 | 组件组合,解析数据,渲染到页面 |
src/pages/CompletedTodos.js | 显示已完成的待办事项的页面 | 组件组合,解析数据,渲染到页面 |
src/components/TodoForm.js | 表单组件,用于添加新任务 | — |
src/components/TodoItem.js | 单项组件,支持完成/删除 | — |
src/components/TodoList.js | 列表组件,渲染多个 TodoItem | — |
注意:组件中 return 内容和 HTML 代码的区别
| 对比项 | HTML | JSX(React 中的”类 HTML”) |
|---|---|---|
| 本质 | 标记语言,浏览器直接解析 | JavaScript 语法扩展,需编译 |
| 文件扩展名 | .html | .jsx 或 .js |
| 属性命名 | class="xxx" | className="xxx"(class 是 JS 关键字) |
| 事件处理 | onclick="handle()" | onClick={handle}(驼峰命名,handle 函数绑定给 onClick 变量) |
| 嵌入逻辑 | 需用 <script> | 可直接用 {} 嵌入 JS 表达式 |
| 闭合标签 | 某些可省略(如 <p>) | 必须闭合:<img />、<br /> |
| 返回内容 | 可以有多个根节点 | 返回一个根元素(如 <div>…</div>),或使用 <>...</> 包含多个 <div> |
项目结构:
todo-app/
├── public/
│ └── index.html
└── src/
├── components/
│ ├── TodoForm.js
│ ├── TodoItem.js
│ └── TodoList.js
├── context/
│ └── TodoContext.js
├── pages/
│ ├── AllTodos.js
│ ├── CompletedTodos.js
│ └── ActiveTodos.js
├── App.js
├── index.js
└── App.css
(1)public/index.html - 应用的 HTML 入口文件:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>最小可用 React 项目</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
(2)src/index.js - 应用的 JavaScript 入口点:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './App.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
(3)src/context/TodoContext.js - 使用 Context API 进行状态管理:
import React, { createContext, useContext, useReducer } from 'react';
// 定义初始状态
const initialState = {
todos: [
{ id: 1, text: '学习 React', completed: false },
{ id: 2, text: '完成项目', completed: true }
]
};
// 定义 action types
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const DELETE_TODO = 'DELETE_TODO';
// Reducer 函数(处理状态更新)
function todoReducer(state, action) {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [
...state.todos,
{
id: Date.now(), // 简单的 ID 生成
text: action.payload,
completed: false
}
]
};
case TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
case DELETE_TODO:
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
default:
return state;
}
}
// 创建 Context
const TodoContext = createContext();
// 自定义 Hook,方便在组件中使用 Context
export function useTodo() {
const context = useContext(TodoContext);
if (!context) {
throw new Error('useTodo must be used within a TodoProvider');
}
return context;
}
// Provider 组件
export function TodoProvider({ children }) {
const [state, dispatch] = useReducer(todoReducer, initialState);
// 将 dispatch 封装成更易用的函数
const addTodo = (text) => dispatch({ type: ADD_TODO, payload: text });
const toggleTodo = (id) => dispatch({ type: TOGGLE_TODO, payload: id });
const deleteTodo = (id) => dispatch({ type: DELETE_TODO, payload: id });
return (
<TodoContext.Provider value={{ ...state, addTodo, toggleTodo, deleteTodo }}>
{children}
</TodoContext.Provider>
);
}
(4)src/components/TodoForm.js - 管理 state(输入框内容),处理事件系统(表单提交和输入变化):
import React, { useState } from 'react';
function TodoForm({ onAdd }) {
// 使用 useState Hook 管理组件内部状态
const [input, setInput] = useState('');
// 处理表单提交事件
const handleSubmit = (e) => {
e.preventDefault(); // 阻止默认提交行为
if (input.trim() !== '') {
onAdd(input); // 调用父组件传递的函数
setInput(''); // 清空输入框
}
};
// 处理输入框变化事件
const handleChange = (e) => {
setInput(e.target.value);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={input}
onChange={handleChange}
placeholder="添加新的待办事项..."
/>
<button type="submit">添加</button>
</form>
);
}
export default TodoForm;
(5)src/components/TodoItem.js - 处理事件系统(点击复选框和删除按钮):
import React from 'react';
function TodoItem({ todo, onToggle, onDelete }) {
return (
<li style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)} // 事件处理
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>删除</button>
</li>
);
}
export default TodoItem;
(6)src/components/TodoList.js - 使用 Hook(useEffect)模拟副作用(例如,当列表更新时在控制台打印):
import React, { useEffect } from 'react';
import TodoItem from './TodoItem';
function TodoList({ todos, onToggle, onDelete }) {
// 使用 useEffect Hook 处理副作用
useEffect(() => {
console.log(`当前待办事项数量: ${todos.length}`);
// 模拟:如果列表为空,打印提示
if (todos.length === 0) {
console.log('待办事项列表为空!');
}
// 清理函数(可选)
return () => {
console.log('TodoList 组件即将卸载或重新渲染');
};
}, [todos]); // 依赖数组:仅在 todos 变化时执行
// 使用 map 方法渲染列表(体现组件复用)
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
}
export default TodoList;
(7)src/pages/AllTodos.js、ActiveTodos.js、CompletedTodos.js - 路由对应的页面组件,使用 useTodo 自定义 Hook 来访问全局状态:
AllTodos.js:
import React from 'react';
import { useTodo } from '../context/TodoContext';
import TodoList from '../components/TodoList';
import TodoForm from '../components/TodoForm';
function AllTodos() {
const { todos, addTodo, toggleTodo, deleteTodo } = useTodo();
return (
<div>
<h2>所有待办事项</h2>
<TodoForm onAdd={addTodo} />
<TodoList
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
</div>
);
}
export default AllTodos;
ActiveTodos.js:
import React from 'react';
import { useTodo } from '../context/TodoContext';
import TodoList from '../components/TodoList';
function ActiveTodos() {
const { todos, toggleTodo, deleteTodo } = useTodo();
const activeTodos = todos.filter(todo => !todo.completed);
return (
<div>
<h2>未完成</h2>
<TodoList
todos={activeTodos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
</div>
);
}
export default ActiveTodos;
CompletedTodos.js:
import React from 'react';
import { useTodo } from '../context/TodoContext';
import TodoList from '../components/TodoList';
function CompletedTodos() {
const { todos, toggleTodo, deleteTodo } = useTodo();
const completedTodos = todos.filter(todo => todo.completed);
return (
<div>
<h2>已完成</h2>
<TodoList
todos={completedTodos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
</div>
);
}
export default CompletedTodos;
(8)src/App.js - 应用的根组件,使用 react-router-dom 进行路由,并使用 TodoProvider 提供状态管理:
import React from 'react';
import { Routes, Route, Link } from 'react-router-dom';
import { TodoProvider } from './context/TodoContext';
import AllTodos from './pages/AllTodos';
import ActiveTodos from './pages/ActiveTodos';
import CompletedTodos from './pages/CompletedTodos';
function App() {
return (
<TodoProvider> {/* 状态管理:提供全局状态 */}
<div className="App">
<h1>待办事项列表</h1>
<nav>
<Link to="/">所有</Link> |
<Link to="/active">未完成</Link> |
<Link to="/completed">已完成</Link>
</nav>
{/* 路由 */}
<Routes>
<Route path="/" element={<AllTodos />} />
<Route path="/active" element={<ActiveTodos />} />
<Route path="/completed" element={<CompletedTodos />} />
</Routes>
</div>
</TodoProvider>
);
}
export default App;
(9)src/App.css(可选):
.App {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
nav {
margin: 20px 0;
}
form {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
input[type="text"] {
flex: 1;
padding: 8px;
}
button {
padding: 8px 16px;
cursor: pointer;
}
ul {
list-style: none;
padding: 0;
}
li {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
li button {
margin-left: auto;
background: #ff4d4f;
color: white;
border: none;
}
运行步骤:
# 创建项目
npx create-react-app todo-app --template javascript
cd todo-app
# 安装路由
npm install react-router-dom
# 启动
npm start
第二章:JSX 语法基础
2.1 什么是 JSX
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| JSX(JavaScript XML) | 一种 JavaScript 的语法扩展,允许在 JavaScript 中编写类似 HTML 的结构。React 推荐使用 JSX 来描述 UI。 | JSX 不是字符串,也不是 HTML,而是会被编译为 React.createElement() 调用。 |
| 语法糖 | JSX 是 React.createElement() 的语法糖,提升代码可读性和开发效率。 | 浏览器不能直接运行 JSX,需通过 Babel 等工具编译为纯 JavaScript。 |
| 必须引入 React | 即使不显式使用 React 变量,使用 JSX 时也必须导入 react,因为 JSX 会被转换为 React.createElement() 调用。 | 在 React 17+,新 JSX 转换允许不显式导入 React,但建议仍保留以兼容性和清晰性。 |
2.2 JSX 与 HTML 的异同
| 对比项 | JSX | HTML | 注意事项 |
|---|---|---|---|
| 标签闭合 | 自闭合标签必须闭合,如 <img />、<input /> | 可省略闭合,如 <img>、<input> | JSX 遵循 XML 规范,所有标签必须闭合。 |
| 属性命名 | 使用驼峰命名法,如 className、onClick | 使用小写,如 class、onclick | class → className,for → htmlFor(因 for 是 JS 保留字)。 |
| 布尔属性 | <input disabled={true} /> 或 <input disabled /> | <input disabled> | 省略值时默认为 true,JSX 中不能写 disabled="disabled"。 |
| 注释 | {/* 注释内容 */} | <!-- 注释内容 --> | JSX 注释必须用 {} 包裹,且使用 /* */ 语法。 |
2.3 在 JSX 中嵌入表达式
| 方法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 嵌入表达式 | {expression} | 在 JSX 中插入任意 JavaScript 表达式(变量、函数调用、运算等)。 | function Greeting() { const name = "Alice"; return (<p>Hello, {name}!</p>); } | 只能使用表达式,不能使用语句(如 if、for)。可用三元运算符或立即执行函数替代。 |
| 嵌入函数调用 | {functionCall()} | 执行函数并插入返回值。 | 见下方示例 | — |
| 嵌入对象(有限) | {object} | 仅可嵌入能转为字符串的对象(如日期),普通对象会报错。 | function ShowDate() { return <p>Today: {new Date().toLocaleDateString()}</p>; } | 不能直接插入普通对象 {name: "John"},会报错:Objects are not valid as a React child。 |
嵌入函数调用示例:
function formatUserName(firstName, lastName) {
return `${firstName} ${lastName}`.trim();
}
function UserProfile({ user }) {
return (
<div>
<h2>User: {formatUserName(user.firstName, user.lastName)}</h2>
<p>Email: {user.email}</p>
</div>
);
}
// 使用示例
function App() {
const currentUser = {
firstName: "John",
lastName: "Doe",
email: "john.doe@example.com"
};
return <UserProfile user={currentUser} />;
}
export default App;
2.4 JSX 属性与动态值
| 方法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 静态属性 | <Element attr="value" /> | 设置固定属性值。 | function StaticImage() { return <img src="/logo.png" alt="Logo" />; } | 适用于不变化的属性。 |
| 动态属性 | <Element attr={value} /> | 使用 {} 插入变量或表达式作为属性值。 | function DynamicImage({ url, desc }) { return <img src={url} alt={desc} />; } | 所有非字符串属性都必须用 {} 包裹。 |
| 布尔属性 | <Element disabled={true} /> | 控制布尔属性的开关。 | function ToggleButton({ isDisabled }) { return <button disabled={isDisabled}>Click me</button>; } | 使用变量控制,避免硬编码。 |
| 展开属性 | <Element {...props} /> | 将对象的所有属性批量传递给组件。 | function ForwardProps() { const imgProps = { src: "/photo.jpg", alt: "A photo", width: 200 }; return <img {...imgProps} />; } | 常用于高阶组件或属性转发,注意避免覆盖显式属性。 |
2.5 JSX 中的样式处理(内联样式、className)
| 方法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 内联样式 | style={{ key: value }} | 使用 JavaScript 对象设置内联样式,属性名使用驼峰命名。 | 见下方示例 | 样式值为字符串或数字(如 fontSize: 24 等价于 24px)。不支持伪类、媒体查询。 |
className | className="class-name" | 使用 CSS 类名应用外部样式表。 | 见下方示例 | 推荐方式,样式与逻辑分离,支持复杂 CSS 规则。 |
| 动态类名 | className={condition ? 'active' : ''} | 根据状态动态切换类名。 | function Button({ isActive }) { return (<button className={isActive ? "btn btn-active" : "btn"}>Submit</button>); } | 可结合 classnames 库简化多个类名的处理。 |
内联样式示例:
function StyledHeader() {
const headerStyle = {
color: "blue",
fontSize: "24px",
textAlign: "center"
};
return <h1 style={headerStyle}>Styled Title</h1>;
}
className 示例:
/* App.css */
.title { font-weight: bold; color: green; }
// Component
import './App.css';
function StyledTitle() {
return <h2 className="title">Hello World</h2>;
}
2.6 JSX 与 JavaScript 的转换原理
| 概念 | 说明 | 注意事项 |
|---|---|---|
| Babel 编译 | JSX 在构建时通过 Babel 转换为 React.createElement() 调用。 | 开发者无需手动调用,但理解转换有助于调试。 |
React.createElement() | React 用于创建虚拟 DOM 节点的底层 API。接收标签名、属性对象、子元素等参数。 | 参数顺序:type, props, ...children |
| 转换示例 | <h1>Hello</h1> → React.createElement('h1', null, 'Hello') | 深层嵌套的 JSX 会转换为嵌套的 createElement 调用。 |
| 方法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
React.createElement | React.createElement(type, props, ...children) | 创建 React 元素(虚拟 DOM 节点)。 | 见下方示例 | 现代开发中极少直接使用,JSX 更简洁。但理解其原理有助于掌握 React 渲染机制。 |
React.createElement 示例:
import React from 'react';
function RawElement() {
return React.createElement(
'div',
{ className: 'container' },
React.createElement('h1', null, 'Title'),
React.createElement('p', null, 'Content')
);
}
第三章:组件与 Props
3.1 函数组件与类组件的定义
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 函数组件 | function ComponentName(props) { return <JSX />; } 或 const ComponentName = (props) => { return <JSX />; }; | 使用 JavaScript 函数定义 React 组件,简洁、易于测试,推荐现代 React 使用。 | 必须首字母大写;必须返回 JSX 或 null;React 16.8+ 支持 Hooks。 |
| 类组件 | class ComponentName extends React.Component { render() { return <JSX />; } } | 使用 ES6 类定义组件,可使用生命周期方法和内部状态(state)。 | 必须继承 React.Component;必须实现 render() 方法;this 需注意绑定。 |
代码示例:
// 函数组件示例
function WelcomeFunction({ name }) {
return <h1>Hello, {name}!</h1>;
}
// 类组件示例
import React from 'react';
class WelcomeClass extends React.Component {
render() {
return <h1>Hello, {this.props.name}!</h1>;
}
}
3.2 组件的复用与组合
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 组件复用 | <Component /> 多次使用 | 将 UI 拆分为独立组件,在不同位置重复使用,减少重复代码。 | 组件应保持独立、可配置(通过 props)。 |
| 组件组合 | <Parent><Child /></Parent> | 通过嵌套组件构建复杂 UI,父组件包含子组件作为 children。 | 利用 props.children 接收并渲染子元素,提升灵活性。 |
代码示例:
// 可复用的按钮组件
function Button({ label, onClick, variant = "primary" }) {
return (
<button className={`btn btn-${variant}`} onClick={onClick}>
{label}
</button>
);
}
// 组合使用多个按钮
function ActionPanel() {
return (
<div className="panel">
<Button label="Save" onClick={() => alert('Saved!')} variant="success" />
<Button label="Delete" onClick={() => alert('Deleted!')} variant="danger" />
</div>
);
}
3.3 Props 的传递与使用
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 传递 Props | <Component propName="value" /> 或 <Component propName={value} /> | 从父组件向子组件传递数据或回调函数。 | 静态值用引号,动态值用 {}。 |
| 接收 Props | function Component(props) { ... } 或 function Component({ name }) { ... } | 子组件接收父组件传入的属性。 | Props 是只读的,子组件不能修改。 |
| 展开传递 Props | <Component {...obj} /> | 将对象的所有属性批量传递给组件。 | 避免传递不必要的属性,可能引发警告。 |
代码示例:
// 子组件:接收并使用 props
function UserProfile({ name, email, onLogout }) {
return (
<div>
<h3>{name}</h3>
<p>{email}</p>
<button onClick={onLogout}>Logout</button>
</div>
);
}
// 父组件:传递 props
function App() {
const user = { name: "Alice", email: "alice@example.com" };
return (
<UserProfile
name={user.name}
email={user.email}
onLogout={() => alert('Logged out!')}
/>
);
}
3.4 Props 的类型检查(PropTypes)
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| PropTypes | Component.propTypes = { propName: PropTypes.string } | 在开发环境中验证传入 props 的类型,防止运行时错误。 | 仅在开发模式生效;需安装 prop-types 包:npm install prop-types。 |
| 常用类型 | PropTypes.string、PropTypes.number、PropTypes.bool、PropTypes.array、PropTypes.object、PropTypes.func、PropTypes.node、PropTypes.element | 定义 props 的期望类型。 | node 可包含任何可渲染内容(字符串、数字、元素等);element 仅限 React 元素。 |
| 必填验证 | PropTypes.string.isRequired | 确保某个 prop 必须被提供。 | 若未传且非必填,默认值可能仍适用。 |
代码示例:
import PropTypes from 'prop-types';
function UserCard({ name, age, avatar, children }) {
return (
<div className="card">
<img src={avatar} alt={name} width="50" />
<h4>{name}</h4>
<p>Age: {age}</p>
{children}
</div>
);
}
UserCard.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
avatar: PropTypes.string,
children: PropTypes.node
};
UserCard.defaultProps = {
avatar: "/default-avatar.png"
};
3.5 组件的默认 Props
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
defaultProps | Component.defaultProps = { propName: value } | 为组件的 props 定义默认值,当父组件未传递时使用。 | 默认值在类型检查之前解析,确保 isRequired 不会因默认值而失效。 |
代码示例:
function Greeting({ name, greeting }) {
return <h2>{greeting}, {name}!</h2>;
}
Greeting.defaultProps = {
greeting: "Hello",
name: "Guest"
};
// 使用时可省略 greeting
function App() {
return (
<>
<Greeting name="Alice" />
<Greeting name="Bob" greeting="Hi" />
</>
);
}
3.6 组件的可组合性与单一职责原则
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
props.children | {children} | 接收并渲染组件标签内的子元素,实现内容插槽。 | 是 React 组合的核心机制,提升组件灵活性。 |
| 单一职责 | 每个组件只负责一个功能或 UI 区块 | 保持组件小而专注,易于测试、复用和维护。 | 避免”巨型组件”;可拆分为 Header、Sidebar、ModalBody 等。 |
| 容器与展示分离 | 容器组件管理逻辑和状态,展示组件仅负责渲染 | 提高展示组件的可复用性和可测试性。 | 展示组件应为纯函数,仅依赖 props。 |
代码示例:
// 展示组件:仅负责渲染
function Card({ title, children, footer }) {
return (
<div className="card">
<div className="card-header">
<h3>{title}</h3>
</div>
<div className="card-body">
{children}
</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
// 容器组件:组合并传递数据
function UserProfileCard({ user }) {
return (
<Card
title={user.name}
footer={<button>Edit</button>}
>
<p>Email: {user.email}</p>
<p>Role: {user.role}</p>
</Card>
);
}
第四章:State 与事件处理
4.1 类组件中的 state 状态管理
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
state | this.state = { key: value } | 类组件中用于存储可变数据的对象,状态变化会触发组件重新渲染。 | state 必须为纯对象;初始化在构造函数或类字段中进行。 |
| 类字段语法(推荐) | state = { count: 0 } | 在类中直接定义 state,无需构造函数,语法更简洁。 | 需 Babel 支持类属性;现代 React 项目普遍使用。 |
代码示例:
import React from 'react';
class Counter extends React.Component {
// 使用类字段语法初始化 state
state = {
count: 0
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
</div>
);
}
}
4.2 状态的初始化与更新(setState)
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
setState(对象形式) | this.setState({ key: value }) | 更新状态的一部分,React 合并新值到现有 state。 | 适用于无依赖的简单更新;不能在更新后立即读取 this.state 获取新值。 |
setState(函数形式) | this.setState((prevState, props) => ({ count: prevState.count + 1 })) | 接收函数,参数为前一个状态和当前 props,返回新状态对象。 | 用于依赖前一个状态的更新(如递增),避免异步更新导致的竞态。 |
代码示例:
import React from 'react';
class Counter extends React.Component {
state = { count: 0 };
increment = () => {
// 使用函数形式确保基于最新状态更新
this.setState((prevState) => ({ count: prevState.count + 1 }));
};
reset = () => {
// 使用对象形式重置状态
this.setState({ count: 0 });
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>+1</button>
<button onClick={this.reset}>Reset</button>
</div>
);
}
}
4.3 事件处理:绑定与调用
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 方法绑定(构造函数) | this.handleClick = this.handleClick.bind(this) | 在构造函数中绑定 this,确保事件处理函数中 this 指向组件实例。 | 冗长,但兼容性好。 |
| 类字段 + 箭头函数(推荐) | handleClick = () => { ... } | 使用箭头函数自动绑定 this,语法简洁,无需手动绑定。 | 现代 React 项目推荐方式。 |
| 内联箭头函数 | <button onClick={() => this.handleClick()}> | 在 JSX 中直接使用箭头函数调用处理函数。 | 每次渲染生成新函数,可能影响性能(尤其在列表中)。 |
代码示例:
import React from 'react';
class ToggleButton extends React.Component {
state = { isOn: false };
// 推荐:使用类字段 + 箭头函数自动绑定 this
handleClick = () => {
this.setState((prevState) => ({ isOn: !prevState.isOn }));
};
render() {
return (
<button onClick={this.handleClick}>
{this.state.isOn ? 'ON' : 'OFF'}
</button>
);
}
}
4.4 向事件处理函数传递参数
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 内联箭头函数 | <button onClick={() => this.handleDelete(id)}>Delete</button> | 在事件触发时传递额外参数(如 ID)。 | 每次渲染创建新函数,可能影响性能。 |
bind 方法 | <button onClick={this.handleEdit.bind(this, id)}>Edit</button> | 使用 bind 预设参数,this 作为第一个参数。 | bind 也会创建新函数,但语法略显冗长。 |
代码示例:
import React from 'react';
class UserList extends React.Component {
users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
handleDelete = (id) => {
console.log('Deleting user:', id);
// 实际项目中调用 API 或更新状态
};
handleEdit = (id) => {
console.log('Editing user:', id);
};
render() {
return (
<ul>
{this.users.map(user => (
<li key={user.id}>
{user.name}
<button onClick={() => this.handleDelete(user.id)}>Delete</button>
<button onClick={this.handleEdit.bind(this, user.id)}>Edit</button>
</li>
))}
</ul>
);
}
}
4.5 状态更新的异步性与批处理
| 名称 | 说明 | 注意事项 |
|---|---|---|
| 异步更新 | this.setState 是异步的,不会立即改变 this.state。 | 不能在 setState 后立即读取 this.state 判断新值。 |
| 批处理(Batching) | React 将多个 setState 调用合并为一次更新,提升性能。 | 在事件处理函数中自动批处理;在异步代码(如 setTimeout、Promise)中可能不批处理(React 18+ 已优化)。 |
| 回调函数(旧式) | this.setState(newState, callback) | 在状态更新并重新渲染后执行回调。 |
代码示例:
import React from 'react';
class AsyncCounter extends React.Component {
state = { count: 0 };
// 错误示范:依赖异步更新
badIncrement = () => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 可能仍为旧值
};
// 正确示范:使用函数形式
goodIncrement = () => {
this.setState(prevState => ({ count: prevState.count + 1 }));
};
// 多次调用会被批处理
tripleIncrement = () => {
this.setState(prevState => ({ count: prevState.count + 1 }));
this.setState(prevState => ({ count: prevState.count + 1 }));
this.setState(prevState => ({ count: prevState.count + 1 }));
// 最终 count 只增加 1(批处理合并)
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.goodIncrement}>+1</button>
<button onClick={this.tripleIncrement}>+3 (batched)</button>
</div>
);
}
}
4.6 函数组件中使用 useState Hook(简介)
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
useState | const [state, setState] = useState(initialValue) | Hook,用于在函数组件中添加状态。返回状态值和更新函数。 | 只能在函数组件顶层调用;不能在条件或循环中使用。 |
| 初始值 | useState(0) 或 useState(() => expensiveCalc()) | 设置初始状态。函数形式用于惰性初始化。 | 惰性初始化适用于昂贵计算,仅在首次渲染执行。 |
| 状态更新 | setState(newValue) 或 setState(prev => newValue) | 更新状态,触发重新渲染。 | 函数形式用于依赖前一个状态的更新。 |
代码示例:
import React, { useState } from 'react';
function Counter() {
// 使用 useState 添加状态
const [count, setCount] = useState(0);
const increment = () => {
setCount(prev => prev + 1); // 推荐使用函数形式
};
const reset = () => {
setCount(0);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={reset}>Reset</button>
</div>
);
}
export default Counter;
第五章:列表渲染与 Keys
5.1 使用 map 渲染列表
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
Array.prototype.map() | array.map((item, index) => <Component key={index} />) | 遍历数组,为每个元素生成 JSX 元素,用于动态渲染列表。 | 必须返回 JSX 或 null;不修改原数组;需为每个元素提供唯一 key。 |
| 条件渲染结合 map | array.map(item => condition ? <Item /> : null) | 在映射过程中根据条件决定是否渲染某项。 | 避免使用 filter 预处理更清晰,除非逻辑简单。 |
代码示例:
import React from 'react';
function TodoList() {
const todos = ['Learn React', 'Build a Project', 'Deploy App'];
return (
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
);
}
export default TodoList;
5.2 Keys 的作用与选择原则
| 名称 | 说明 | 注意事项 |
|---|---|---|
| Key 的作用 | React 使用 key 来识别列表中每个元素的唯一性,帮助 React 确定哪些元素被添加、删除或重新排序,从而高效更新 DOM。 | key 是同级元素之间的标识,不传递给组件的 props。 |
| 选择原则:使用稳定唯一 ID | 优先使用数据中的唯一标识符,如 id、userId、productId。 | 推荐方案,确保 key 稳定、可预测、唯一。 |
| 备选方案:使用索引(index) | 当数据无稳定 ID 且列表静态、无排序、无增删时,可使用数组索引。 | ❌ 不推荐用于动态列表,会导致性能问题和状态错乱。 |
| 避免使用随机数 | 如 Math.random() 或 Date.now()。 | 每次渲染 key 都变化,导致 React 重新创建所有 DOM,失去优化意义。 |
代码示例:
import React from 'react';
function UserList() {
const users = [
{ id: 101, name: 'Alice' },
{ id: 102, name: 'Bob' },
{ id: 103, name: 'Charlie' }
];
return (
<ul>
{users.map(user => (
// ✅ 正确:使用稳定唯一的 id 作为 key
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export default UserList;
5.3 Keys 的使用场景与注意事项
| 场景/注意事项 | 说明 |
|---|---|
| 必须在 map 中提供 key | React 会警告:Each child in a list should have a unique "key" prop. |
| Key 必须在兄弟节点中唯一 | 只需在同一 map 调用的兄弟元素中唯一,不同列表的 key 可重复。 |
| Key 不会传递给组件 | 子组件无法通过 props.key 访问 key。如需 ID,应显式传递:<User key={id} id={id} />。 |
| 不要在子组件内部使用 index 作为 key | 即使子组件封装了列表,key 也应在 map 外部指定。 |
| Fragment 也需要 key | 当 map 返回多个元素(如 <React.Fragment>)时,每个 Fragment 需要 key。 |
代码示例:
import React from 'react';
function MessageList() {
const messages = [
{ id: 1, text: 'Hello', sender: 'Alice' },
{ id: 2, text: 'Hi', sender: 'Bob' }
];
return (
<div>
{messages.map(message => (
// ✅ 正确:为每个 Fragment 提供 key
<React.Fragment key={message.id}>
<strong>{message.sender}:</strong>
<span> {message.text}</span>
<br />
</React.Fragment>
))}
</div>
);
}
// 子组件接收显式传递的 id
function UserItem({ user }) {
return <li>{user.name}</li>;
}
function App() {
const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
return (
<ul>
{users.map(user => (
// ✅ 正确:key 在 map 中指定,id 显式传递
<UserItem key={user.id} user={user} />
))}
</ul>
);
}
5.4 列表项中状态管理的常见问题
| 问题 | 描述 | 解决方案 | 注意事项 |
|---|---|---|---|
| 使用索引作为 key 导致状态错乱 | 当列表可排序、过滤或增删时,使用 index 作为 key 会导致 React 错误复用 DOM,组件状态(如输入框内容)错位。 | ✅ 使用数据中的唯一 ID 作为 key。 | 这是最常见的列表渲染 bug。 |
| 在列表项中使用 useState 导致状态与索引绑定 | 如果状态基于索引存储(如 useState 在循环内),删除项后状态会整体前移,导致错乱。 | ✅ 使用唯一 ID 管理状态(如对象映射 state[id]),或将状态提升到父组件。 | — |
| 未正确清理副作用 | 列表项中使用 useEffect(函数组件)时,未返回清理函数,可能导致内存泄漏。 | ✅ 在 useEffect 中返回清理函数。 | — |
代码示例:错误示范 vs 正确示范
import React, { useState } from 'react';
// ❌ 错误示范:使用索引作为 key,状态与索引绑定
function BadList() {
const [items, setItems] = useState(['A', 'B', 'C']);
const [edits, setEdits] = useState({}); // 用对象存储编辑状态
const removeItem = (index) => {
const newItems = items.filter((_, i) => i !== index);
setItems(newItems);
// ❌ 问题:删除后,后续项的索引变化,但 edits 状态仍按原索引存储,导致错乱
};
return (
<ul>
{items.map((item, index) => (
<li key={index}> {/* ❌ 使用 index 作为 key */}
<input
value={edits[index] || ''}
onChange={(e) => setEdits(prev => ({ ...prev, [index]: e.target.value }))}
/>
<button onClick={() => removeItem(index)}>Remove</button>
</li>
))}
</ul>
);
}
// ✅ 正确示范:使用唯一 ID,状态与 ID 绑定
function GoodList() {
const [items, setItems] = useState([
{ id: 1, value: 'A' },
{ id: 2, value: 'B' },
{ id: 3, value: 'C' }
]);
const [edits, setEdits] = useState({}); // 状态基于 id
const removeItem = (id) => {
setItems(prev => prev.filter(item => item.id !== id));
};
return (
<ul>
{items.map(item => (
<li key={item.id}> {/* ✅ 使用唯一 id 作为 key */}
<input
value={edits[item.id] || ''}
onChange={(e) => setEdits(prev => ({ ...prev, [item.id]: e.target.value }))}
/>
<button onClick={() => removeItem(item.id)}>Remove</button>
</li>
))}
</ul>
);
}
export default GoodList;
第六章:表单处理
6.1 受控组件(Controlled Components)
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 受控组件 | <input value={state} onChange={handleChange} /> | 表单元素的值由 React 状态(state)控制,通过 onChange 同步更新状态。 | 值必须由 state 提供,不能为 undefined 或 null(除非是初始值)。 |
| 状态驱动 | 使用 useState 管理表单数据 | 将表单数据集中管理,便于验证、提交和重置。 | 数据始终与 UI 同步,符合 React 声明式理念。 |
代码示例:
import React, { useState } from 'react';
function NameForm() {
const [name, setName] = useState('');
const handleChange = (e) => {
setName(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
alert(`Submitted name: ${name}`);
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" value={name} onChange={handleChange} />
</label>
<button type="submit">Submit</button>
</form>
);
}
export default NameForm;
6.2 处理文本输入、单选框、复选框、下拉框
| 输入类型 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 文本输入 | <input type="text" value={value} onChange={handleChange} /> | 捕获用户输入的文本。 | value 必须绑定到状态。 |
| 单选框(Radio) | <input type="radio" checked={checked} onChange={handleChange} /> | 从多个选项中选择一项。 | 使用 checked 而非 value 控制选中状态。 |
| 复选框(Checkbox) | <input type="checkbox" checked={checked} onChange={handleChange} /> | 切换布尔值(是/否)。 | e.target.checked 为布尔值,e.target.value 为字符串。 |
| 下拉框(Select) | <select value={selected} onChange={handleChange}> | 从下拉列表中选择一项或多选。 | 单选时 value 为字符串;多选时为数组。 |
代码示例:
import React, { useState } from 'react';
function UserProfileForm() {
const [formData, setFormData] = useState({
username: '',
gender: 'male',
subscribed: false,
country: 'CN'
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form Data:', formData);
};
return (
<form onSubmit={handleSubmit}>
<label>
Username:
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
/>
</label>
<label>
Gender:
<label><input type="radio" name="gender" value="male" checked={formData.gender === 'male'} onChange={handleChange} /> Male</label>
<label><input type="radio" name="gender" value="female" checked={formData.gender === 'female'} onChange={handleChange} /> Female</label>
</label>
<label>
<input
type="checkbox"
name="subscribed"
checked={formData.subscribed}
onChange={handleChange}
/>
Subscribe to newsletter
</label>
<label>
Country:
<select name="country" value={formData.country} onChange={handleChange}>
<option value="US">United States</option>
<option value="CN">China</option>
<option value="DE">Germany</option>
</select>
</label>
<button type="submit">Save</button>
</form>
);
}
export default UserProfileForm;
6.3 多个输入的统一处理
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 统一事件处理器 | handleChange = (e) => { const { name, value } = e.target; setState({ [name]: value }); } | 使用一个函数处理多个输入,通过 name 属性区分字段。 | 所有输入必须有 name 属性;适用于简单表单。 |
| 解构赋值 | const { name, value, type, checked } = e.target | 简化事件对象属性访问。 | 处理复选框时注意 checked 与 value 的区别。 |
代码示例:
import React, { useState } from 'react';
function ContactForm() {
const [form, setForm] = useState({
name: '',
email: '',
phone: '',
message: ''
});
const handleInputChange = (e) => {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = (e) => {
e.preventDefault();
alert(`Message from ${form.name}: ${form.message}`);
};
return (
<form onSubmit={handleSubmit}>
<input name="name" value={form.name} onChange={handleInputChange} placeholder="Your Name" />
<input name="email" value={form.email} onChange={handleInputChange} placeholder="Email" type="email" />
<input name="phone" value={form.phone} onChange={handleInputChange} placeholder="Phone" type="tel" />
<textarea name="message" value={form.message} onChange={handleInputChange} placeholder="Message" />
<button type="submit">Send</button>
</form>
);
}
export default ContactForm;
6.4 非受控组件与 Ref 的使用(简介)
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
useRef Hook | const inputRef = useRef(); | 创建对 DOM 元素的引用,用于直接访问表单值。 | 不推荐用于常规表单,仅在特定场景(如文件输入、聚焦、第三方库集成)使用。 |
| 非受控组件 | <input defaultValue="initial" ref={inputRef} /> | 值不由 React 状态控制,通过 ref 在提交时读取。 | 使用 defaultValue 而非 value;避免与受控组件混合。 |
代码示例:
import React, { useRef } from 'react';
function FileUploadForm() {
const fileInputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const file = fileInputRef.current.files[0];
if (file) {
alert(`Selected file: ${file.name} (${file.size} bytes)`);
} else {
alert('No file selected');
}
};
return (
<form onSubmit={handleSubmit}>
<label>
Upload File:
<input type="file" ref={fileInputRef} />
</label>
<button type="submit">Upload</button>
</form>
);
}
export default FileUploadForm;
6.5 表单验证基础
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 内联验证(HTML5) | <input required minLength="2" pattern="[a-zA-Z]+" /> | 使用浏览器内置验证。 | 简单有效,但样式和提示不可定制。 |
| 手动验证 | 在 onChange 或 onSubmit 中检查值,设置错误状态。 | 实现自定义验证逻辑和错误提示。 | 验证状态应与表单状态分离管理。 |
| 错误状态管理 | const [errors, setErrors] = useState({}) | 存储每个字段的验证错误信息。 | 提交时集中验证,输入时可做即时反馈。 |
代码示例:
import React, { useState } from 'react';
function ValidatedForm() {
const [form, setForm] = useState({ email: '', password: '' });
const [errors, setErrors] = useState({});
const validate = () => {
const newErrors = {};
if (!form.email) newErrors.email = 'Email is required';
else if (!/\S+@\S+\.\S+/.test(form.email)) newErrors.email = 'Email is invalid';
if (!form.password) newErrors.password = 'Password is required';
else if (form.password.length < 6) newErrors.password = 'Password must be at least 6 characters';
return newErrors;
};
const handleChange = (e) => {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
// 可选:输入时清除对应错误
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: undefined }));
}
};
const handleSubmit = (e) => {
e.preventDefault();
const newErrors = validate();
if (Object.keys(newErrors).length === 0) {
alert('Form submitted successfully!');
} else {
setErrors(newErrors);
}
};
return (
<form onSubmit={handleSubmit}>
<label>
Email:
<input
type="email"
name="email"
value={form.email}
onChange={handleChange}
/>
{errors.email && <span style={{ color: 'red' }}>{errors.email}</span>}
</label>
<label>
Password:
<input
type="password"
name="password"
value={form.password}
onChange={handleChange}
/>
{errors.password && <span style={{ color: 'red' }}>{errors.password}</span>}
</label>
<button type="submit">Login</button>
</form>
);
}
export default ValidatedForm;
第七章:条件渲染
7.1 if 语句与条件逻辑
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
if 语句 | if (condition) { return <JSX />; } | 在组件函数内部使用 if 判断,决定返回哪个 JSX。 | 适用于复杂逻辑或多分支条件;必须在函数作用域内使用。 |
| 早期返回 | if (!prop) return null; | 在渲染前检查必要条件,提前返回 null 或占位内容。 | 提升代码可读性,避免深层嵌套。 |
代码示例:
import React from 'react';
function Greeting({ user }) {
if (user) {
return <h1>Welcome back, {user.name}!</h1>;
} else {
return <h1>Please sign in.</h1>;
}
}
function Dashboard({ isLoggedIn }) {
// 早期返回
if (!isLoggedIn) {
return <div>Please log in to view dashboard.</div>;
}
return (
<div>
<h2>Dashboard</h2>
<p>Your data is secure.</p>
</div>
);
}
export default Dashboard;
7.2 与运算符 &&
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 逻辑与(&&) | {condition && <Component />} | 当 condition 为真时渲染右侧的 JSX,否则不渲染(返回 null)。 | 简洁,适用于”存在即渲染”的场景;左侧不能为 0、false、null 等,否则会渲染出来。 |
代码示例:
import React, { useState } from 'react';
function Notification() {
const [hasNewMessage, setHasNewMessage] = useState(true);
return (
<div>
<h3>Inbox</h3>
{/* 只有当有新消息时才显示提示 */}
{hasNewMessage && <p style={{ color: 'blue' }}>You have a new message!</p>}
<button onClick={() => setHasNewMessage(false)}>Mark as Read</button>
</div>
);
}
export default Notification;
7.3 三元运算符
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 三元运算符 | {condition ? <A /> : <B />} | 根据条件真假选择渲染两个组件中的一个。 | 适用于二选一渲染;比 if 更简洁,适合 JSX 内联使用。 |
| 嵌套三元 | {cond1 ? A : cond2 ? B : C} | 多条件判断。 | ❌ 不推荐,可读性差,应使用 if 或 switch 替代。 |
代码示例:
import React, { useState } from 'react';
function ToggleContent() {
const [isLoaded, setIsLoaded] = useState(false);
return (
<div>
{/* 根据加载状态显示不同内容 */}
{isLoaded ? (
<div>
<h2>Data Loaded</h2>
<p>Your content is ready.</p>
</div>
) : (
<p>Loading...</p>
)}
<button onClick={() => setIsLoaded(!isLoaded)}>
{isLoaded ? 'Reset' : 'Load Data'}
</button>
</div>
);
}
export default ToggleContent;
7.4 阻止组件渲染(返回 null)
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
返回 null | return null; | 从 render 或函数组件中返回 null,阻止该组件在 DOM 中渲染任何内容。 | 合法且常用;React 不会渲染 null,也不会产生副作用。 |
| 条件性包装组件 | 组件根据 props 决定是否渲染子元素。实现权限控制、加载状态等。 | 父组件可控制子组件的可见性。 | — |
代码示例:
import React from 'react';
// 阻止自身渲染
function AdminPanel({ isAdmin }) {
if (!isAdmin) {
// 不渲染任何内容
return null;
}
return (
<div className="admin-panel">
<h3>Admin Controls</h3>
<button>Delete User</button>
</div>
);
}
// 条件性包装组件
function Visible({ when, children }) {
return when ? children : null;
}
function App() {
const user = { isAdmin: false };
return (
<div>
<h1>My App</h1>
{/* 使用包装组件 */}
<Visible when={user.isAdmin}>
<AdminPanel />
</Visible>
</div>
);
}
export default App;
7.5 条件渲染的最佳实践
| 实践 | 说明 | 示例 |
|---|---|---|
优先使用 && 和三元 | 在 JSX 内部,优先使用 && 和 ? : 实现简单条件渲染,保持模板清晰。 | {isLoggedIn && <Dashboard />} |
| 复杂逻辑提取到变量或函数 | 避免在 JSX 中写复杂判断,提前计算好要渲染的内容。 | const content = getUserContent(); return <div>{content}</div>; |
| 避免在条件中渲染 0 或空字符串 | 0 && <div /> 会渲染 0 而非组件,因 0 为假但会被显示。 | 使用 !!value 或明确比较。 |
| 使用组件拆分 | 将不同状态的 UI 拆分为独立组件,提高可复用性。 | <Loading />、<Error />、<Success /> |
代码示例:
import React, { useState } from 'react';
// ✅ 最佳实践:将复杂条件逻辑提取
function UserData({ status, data }) {
// 提前计算内容,避免 JSX 中复杂逻辑
const getContent = () => {
switch (status) {
case 'loading':
return <p>Loading user data...</p>;
case 'error':
return <p style={{ color: 'red' }}>Failed to load data.</p>;
case 'success':
return (
<div>
<h3>{data.name}</h3>
<p>Email: {data.email}</p>
</div>
);
default:
return null;
}
};
const content = getContent();
return (
<div>
<h2>User Profile</h2>
{/* JSX 内部简洁明了 */}
{content}
</div>
);
}
function App() {
const [user, setUser] = useState({ status: 'loading' });
const loadUser = () => {
setUser({
status: 'success',
data: { name: 'Alice', email: 'alice@example.com' }
});
};
return (
<UserData status={user.status} data={user.data} />
);
}
export default App;
第八章:组件的生命周期(类组件)
8.1 挂载阶段(Mounting)生命周期方法
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
constructor | constructor(props) { super(props); this.state = {}; } | 初始化 state 和绑定事件处理函数。 | 必须调用 super(props);不能调用 setState。 |
static getDerivedStateFromProps | static getDerivedStateFromProps(props, state) | 将 props 同步到 state(罕见需求)。 | 静态方法,无 this;返回 null 或状态对象;避免副作用。 |
render | render() { return <JSX />; } | 必须实现,返回要渲染的 JSX。 | 纯函数,不能修改 state 或调用 setState。 |
componentDidMount | componentDidMount() { ... } | 组件挂载后执行,适合发起网络请求、设置定时器、操作 DOM。 | 可安全调用 setState,触发更新。 |
代码示例:
import React from 'react';
class UserProfile extends React.Component {
state = { user: null, loading: true };
componentDidMount() {
// 模拟 API 请求
setTimeout(() => {
this.setState({
user: { id: 1, name: 'Alice', email: 'alice@example.com' },
loading: false
});
}, 1000);
}
render() {
const { loading, user } = this.state;
if (loading) return <p>Loading...</p>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
}
export default UserProfile;
8.2 更新阶段(Updating)生命周期方法
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
static getDerivedStateFromProps | static getDerivedStateFromProps(props, state) | 在 props 或 state 更新时调用,用于同步数据。 | 与挂载阶段相同;避免在此执行副作用。 |
shouldComponentUpdate | shouldComponentUpdate(nextProps, nextState) | 性能优化,控制组件是否重新渲染。返回 false 可跳过渲染。 | 返回 false 时,render 和后续生命周期不执行;默认返回 true。 |
render | render() { ... } | 重新渲染组件。 | 同挂载阶段。 |
getSnapshotBeforeUpdate | getSnapshotBeforeUpdate(prevProps, prevState) | 在 DOM 更新前获取快照(如滚动位置),返回值传给 componentDidUpdate。 | 不常用;用于保存更新前的 DOM 状态。 |
componentDidUpdate | componentDidUpdate(prevProps, prevState, snapshot) | 组件更新后执行,适合根据 props 变化发起请求或操作 DOM。 | 可调用 setState,但必须在条件判断中,否则导致无限循环。 |
代码示例:
import React from 'react';
class ChatList extends React.Component {
state = { messages: [] };
componentDidUpdate(prevProps) {
// 当 roomId 变化时,加载新房间的消息
if (prevProps.roomId !== this.props.roomId) {
this.loadMessages(this.props.roomId);
}
}
loadMessages = (roomId) => {
// 模拟加载消息
this.setState({ messages: [`Message 1 in ${roomId}`, `Message 2 in ${roomId}`] });
};
render() {
return (
<div>
<h3>Room: {this.props.roomId}</h3>
<ul>
{this.state.messages.map((msg, i) => (
<li key={i}>{msg}</li>
))}
</ul>
</div>
);
}
}
export default ChatList;
8.3 卸载阶段(Unmounting)生命周期方法
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
componentWillUnmount | componentWillUnmount() { ... } | 组件卸载前执行,用于清理资源:清除定时器、取消网络请求、移除事件监听器。 | 不能调用 setState,因为组件即将被销毁。 |
代码示例:
import React from 'react';
class Timer extends React.Component {
state = { seconds: 0 };
componentDidMount() {
// 设置定时器
this.interval = setInterval(() => {
this.setState(prev => ({ seconds: prev.seconds + 1 }));
}, 1000);
}
componentWillUnmount() {
// 清理定时器,防止内存泄漏
clearInterval(this.interval);
}
render() {
return <p>Timer: {this.state.seconds}s</p>;
}
}
export default Timer;
8.4 错误处理(Error Boundaries)
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
static getDerivedStateFromError | static getDerivedStateFromError(error) | 在子组件抛出错误后调用,返回新的 state 以显示降级 UI。 | 静态方法,用于更新状态;不能访问 this。 |
componentDidCatch | componentDidCatch(error, info) | 捕获错误信息和组件栈,适合记录错误日志。 | 可用于上报错误到监控服务。 |
代码示例:
import React from 'react';
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
// 更新 state,让下一次渲染显示降级 UI
return { hasError: true, error };
}
componentDidCatch(error, info) {
// 例如:发送错误日志到服务器
console.error('Error caught by boundary:', error, info.componentStack);
}
render() {
if (this.state.hasError) {
return (
<div style={{ color: 'red' }}>
<h2>Something went wrong.</h2>
<details>{this.state.error?.toString()}</details>
</div>
);
}
return this.props.children;
}
}
// 使用示例
function BrokenComponent() {
throw new Error('I crashed!');
}
function App() {
return (
<ErrorBoundary>
<BrokenComponent />
</ErrorBoundary>
);
}
export default App;
8.5 生命周期方法的执行顺序与使用场景
| 阶段 | 方法调用顺序 | 典型使用场景 |
|---|---|---|
| 挂载(Mounting) | constructor → getDerivedStateFromProps → render → componentDidMount | 初始化状态、发起首次数据请求、设置监听器。 |
| 更新(Updating) | getDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate → componentDidUpdate | 响应 props 变化、性能优化、更新后操作 DOM。 |
| 卸载(Unmounting) | componentWillUnmount | 清理定时器、取消订阅、移除事件监听。 |
| 错误处理 | getDerivedStateFromError → componentDidCatch → render | 捕获渲染错误,显示友好错误界面。 |
完整生命周期示例:
import React from 'react';
class LifecycleDemo extends React.Component {
state = { count: 0 };
constructor(props) {
super(props);
console.log('1. constructor');
}
static getDerivedStateFromProps(props, state) {
console.log('2. getDerivedStateFromProps');
return null;
}
componentDidMount() {
console.log('4. componentDidMount');
}
shouldComponentUpdate(nextProps, nextState) {
console.log('5. shouldComponentUpdate');
return true;
}
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('6. getSnapshotBeforeUpdate');
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('7. componentDidUpdate');
}
componentWillUnmount() {
console.log('8. componentWillUnmount');
}
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
console.log('3. render');
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick}>Increment</button>
</div>
);
}
}
export default LifecycleDemo;
第九章:useState Hook
9.1 useState 基本语法与使用
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
useState | const [state, setState] = useState(initialValue); | 声明一个状态变量及其更新函数。 | 只能在函数组件顶层调用,不能在条件或循环中使用。 |
| 状态读取 | {state} | 在 JSX 中使用当前状态值。 | 状态是不可变的,每次更新都会生成新值。 |
| 状态更新 | setState(newValue) | 更新状态,触发组件重新渲染。 | 新值会替换旧值(非合并),函数组件无 this.state。 |
代码示例:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
export default Counter;
9.2 状态的初始化(函数形式)
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 函数初始化 | useState(() => expensiveCalculation()) | 延迟执行昂贵的计算,仅在组件首次渲染时运行。 | 避免每次渲染都执行高成本操作;传入函数,不是调用函数。 |
代码示例:
import React, { useState } from 'react';
function UserProfile({ userId }) {
// ✅ 正确:函数形式,只在首次渲染时计算
const [user, setUser] = useState(() => {
console.log('Fetching user data...');
return { id: userId, name: 'Alice', profileLoaded: false };
});
return (
<div>
<h2>Welcome, {user.name}</h2>
<p>ID: {user.id}</p>
</div>
);
}
export default UserProfile;
9.3 函数式更新状态
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 函数式更新 | setState(prevState => prevState + 1) | 使用前一个状态值计算新状态,确保更新基于最新值。 | 当新状态依赖于旧状态时必须使用,避免闭包问题。 |
代码示例:
import React, { useState } from 'react';
function BatchCounter() {
const [count, setCount] = useState(0);
// ❌ 错误:基于闭包中的旧 count 值,点击一次只会加 1
const badIncrement = () => {
setCount(count + 1);
setCount(count + 1); // 仍然基于旧值
};
// ✅ 正确:函数式更新,每次基于最新状态
const goodIncrement = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={badIncrement}>Bad +2</button> {/* 实际只 +1 */}
<button onClick={goodIncrement}>Good +2</button> {/* 正确 +2 */}
</div>
);
}
export default BatchCounter;
9.4 处理对象与数组状态
| 数据类型 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 对象状态 | setState({ ...prevState, key: value }) | 更新对象状态的特定字段。 | 不能直接修改原对象;必须使用展开运算符创建新对象。 |
| 数组状态 | setState([...prevState, newItem]) | 添加、删除或修改数组元素。 | 避免使用 push、pop、splice 等变异方法;使用 filter、map、concat 等纯函数。 |
代码示例:
import React, { useState } from 'react';
function TodoApp() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', completed: false }
]);
const [input, setInput] = useState('');
const addTodo = () => {
if (input.trim()) {
const newTodo = {
id: Date.now(),
text: input,
completed: false
};
// ✅ 使用展开运算符创建新数组
setTodos(prev => [...prev, newTodo]);
setInput('');
}
};
const toggleTodo = (id) => {
// ✅ 使用 map 创建新数组,不修改原数组
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const removeTodo = (id) => {
// ✅ 使用 filter 创建新数组
setTodos(prev => prev.filter(todo => todo.id !== id));
};
return (
<div>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Add a todo"
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
export default TodoApp;
9.5 常见陷阱与最佳实践
| 陷阱/实践 | 说明 | 解决方案 |
|---|---|---|
| 直接修改状态 | 如 state.push() 或 state.name = 'new'。 | ❌ 会导致 React 无法检测到变化,UI 不更新。✅ 始终返回新对象/数组。 |
| 忽略函数式更新 | 在事件处理中多次 setState 依赖旧状态。 | ✅ 使用 (prev) => prev + 1 形式。 |
| 状态合并问题 | useState 不像 this.setState 会自动合并对象。 | ✅ 手动使用 {...prevState, ...newValues} 合并。 |
| 多个状态的拆分 | 将不相关的状态合并到一个对象中。 | ✅ 拆分为多个 useState 调用,提高可维护性。 |
| 状态提升 | 多个组件需要共享状态时。 | ✅ 将状态提升到最近的共同父组件。 |
代码示例:最佳实践 - 拆分状态
import React, { useState } from 'react';
// ✅ 好:拆分不相关的状态
function UserForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(18);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
// 模拟提交
await new Promise(resolve => setTimeout(resolve, 1000));
alert(`Submitted: ${name}, ${email}, ${age}`);
setIsSubmitting(false);
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} placeholder="Name" required />
<input value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" type="email" required />
<input value={age} onChange={e => setAge(Number(e.target.value))} type="number" />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
export default UserForm;
第十章:useEffect Hook
10.1 useEffect 基本语法
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
useEffect | useEffect(() => { /* effect */ }, [dependencies]); | 执行副作用(如数据获取、订阅、手动 DOM 操作)。 | 必须在函数组件顶层调用;回调函数不能是 async。 |
| 副作用函数 | () => { ... } | 包含要执行的副作用逻辑。 | 可返回一个清理函数。 |
| 依赖数组 | [dep1, dep2] | 控制 effect 的执行时机。 | 依赖项必须包含所有在 effect 中使用的响应式值(props、state)。 |
代码示例:
import React, { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
// 每秒更新一次
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// 清理函数
return () => clearInterval(interval);
}, []); // 依赖数组为空,只在挂载时执行
return <p>Timer: {seconds}s</p>;
}
export default Timer;
10.2 模拟 componentDidMount
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 空依赖数组 | useEffect(() => { ... }, []) | 在组件首次渲染后执行一次,等效于 componentDidMount。 | 常用于初始化数据获取、设置订阅、记录分析等。 |
| 初始化副作用 | 在 effect 中发起 API 请求或设置监听。避免在组件体中直接执行副作用。 | effect 在渲染后异步执行。 | — |
代码示例:
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// ✅ 模拟 componentDidMount:只在首次渲染后执行
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, []); // 依赖数组为空
return (
<div>
{user ? <h2>{user.name}</h2> : <p>Loading...</p>}
</div>
);
}
export default UserProfile;
10.3 模拟 componentDidUpdate
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 非空依赖数组 | useEffect(() => { ... }, [prop, state]) | 当依赖项变化时执行,等效于 componentDidUpdate。 | 避免遗漏依赖,否则可能使用过时值。 |
| 响应 props/state 变化 | 根据 props 或 state 的变化重新获取数据或更新 DOM。不要用于所有更新,仅当依赖特定值时。 | 可能导致频繁执行,注意性能。 | — |
代码示例:
import React, { useState, useEffect } from 'react';
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
// ✅ 当 roomId 变化时,重新加载消息
useEffect(() => {
if (!roomId) return;
fetch(`/api/rooms/${roomId}/messages`)
.then(res => res.json())
.then(data => setMessages(data));
}, [roomId]); // 依赖 roomId
return (
<div>
<h3>Room: {roomId}</h3>
<ul>
{messages.map((msg, i) => <li key={i}>{msg}</li>)}
</ul>
</div>
);
}
export default ChatRoom;
10.4 模拟 componentWillUnmount
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 返回清理函数 | useEffect(() => { return () => { /* cleanup */ }; }, []) | 在组件卸载前执行清理逻辑,等效于 componentWillUnmount。 | 必须在 effect 内部返回函数。 |
| 清理资源 | 清除定时器、取消网络请求、移除事件监听器。防止内存泄漏和无效状态更新。 | 清理函数在每次 effect 重新执行前也会调用。 | — |
代码示例:
import React, { useState, useEffect } from 'react';
function MouseTracker() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
// 添加事件监听
window.addEventListener('mousemove', handleMove);
// ✅ 清理函数:移除事件监听
return () => {
window.removeEventListener('mousemove', handleMove);
};
}, []); // 只在挂载/卸载时执行
return (
<p>Mouse at: {position.x}, {position.y}</p>
);
}
export default MouseTracker;
10.5 依赖数组的作用与优化
| 依赖类型 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
空数组 [] | useEffect(() => {}, []) | 只在挂载时执行一次。 | 确保 effect 内不引用任何 props/state,否则会捕获初始值。 |
有依赖 [a, b] | useEffect(() => {}, [a, b]) | 当 a 或 b 变化时重新执行。 | 必须包含所有使用的响应式值。 |
| 无依赖(省略) | useEffect(() => {}) | 每次渲染后都执行。 | 很少使用,可能导致性能问题。 |
代码示例:
import React, { useState, useEffect } from 'react';
function Example({ userId, theme }) {
const [data, setData] = useState(null);
// 仅当 userId 变化时重新获取数据
useEffect(() => {
fetch(`/api/users/${userId}/profile`)
.then(res => res.json())
.then(setData);
}, [userId]); // ✅ 正确依赖
// 仅当 theme 变化时更新文档标题
useEffect(() => {
document.title = `App - ${theme}`;
}, [theme]); // ✅ 正确依赖
return <div>{data ? data.name : 'Loading...'}</div>;
}
export default Example;
10.6 清理副作用(返回清理函数)
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 清理函数 | return () => { /* cleanup logic */ }; | 清理上一次 effect 创建的资源。 | 每次 effect 执行前都会先调用上一次的清理函数。 |
| 避免内存泄漏 | 确保定时器、订阅、监听器被正确移除。尤其在频繁重新渲染的组件中至关重要。 | 清理函数在组件卸载时也会执行。 | — |
代码示例:
import React, { useState, useEffect } from 'react';
function DataPoller({ url }) {
const [data, setData] = useState(null);
useEffect(() => {
let cancelled = false; // 标记是否已取消
const poll = async () => {
while (!cancelled) {
const res = await fetch(url);
const json = await res.json();
if (!cancelled) {
setData(json);
}
await new Promise(resolve => setTimeout(resolve, 5000)); // 5s 轮询
}
};
poll();
// ✅ 清理函数:设置 cancelled 为 true,停止轮询
return () => {
cancelled = true;
};
}, [url]);
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}
export default DataPoller;
10.7 常见错误:无限循环、遗漏依赖
| 错误类型 | 说明 | 解决方案 |
|---|---|---|
| 无限循环 | useEffect 中更新了依赖数组中的状态,且无终止条件。 | ✅ 添加条件判断;使用函数式更新;检查依赖是否必要。 |
| 遗漏依赖 | effect 使用了 props/state 但未加入依赖数组,导致闭包问题。 | ✅ 使用 ESLint 插件 eslint-plugin-react-hooks 自动检测;或使用 useCallback/useMemo 包装。 |
错误地使用 async | 直接将 effect 回调设为 async。 | ✅ 在 effect 内部定义 async 函数并调用。 |
代码示例:修复常见错误
import React, { useState, useEffect } from 'react';
function CorrectedExample({ userId }) {
const [user, setUser] = useState(null);
// ✅ 正确:在 effect 内部使用 async 函数
useEffect(() => {
const fetchUser = async () => {
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
setUser(data);
};
fetchUser();
}, [userId]); // ✅ 正确添加 userId 依赖
// ❌ 错误示例(注释掉):
// useEffect(async () => { ... }, [userId]); // 不能是 async
return <div>{user?.name || 'Loading...'}</div>;
}
export default CorrectedExample;
第十一章:其他常用 Hooks
11.1 useRef Hook
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
useRef | const ref = useRef(initialValue); | 创建一个可变的引用对象,其 .current 属性可存储任意可变值(如 DOM 元素、计时器 ID、任意值)。 | ref.current 变化不会触发重新渲染;可用于访问 DOM 或存储”实例变量”。 |
| 访问 DOM 元素 | useRef() + ref={ref} | 获取对真实 DOM 节点的引用,用于聚焦、测量等。 | 必须将 ref 传递给 JSX 元素。 |
代码示例:
import React, { useRef, useEffect } from 'react';
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// 使用 ref.current 访问 DOM 节点
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
export default TextInputWithFocusButton;
11.2 useContext Hook
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
useContext | const value = useContext(MyContext); | 订阅并读取 React 上下文(Context)的当前值。 | 必须先通过 React.createContext 创建上下文;组件会重新渲染当上下文值变化。 |
| 消费上下文 | 在深层嵌套组件中传递数据,避免”props 逐层传递”。适用于主题、用户认证、语言等全局状态。 | 不要滥用,优先考虑组件组合。 | — |
代码示例:
import React, { createContext, useContext } from 'react';
// 创建上下文
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
// 使用 useContext 读取当前主题
const theme = useContext(ThemeContext);
return <button style={{ background: theme === 'dark' ? '#333' : '#fff' }}>I am {theme}</button>;
}
export default App;
11.3 useReducer Hook
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
useReducer | const [state, dispatch] = useReducer(reducer, initialState); | 管理复杂状态逻辑,通过 dispatch(action) 触发状态更新。 | 适用于状态逻辑复杂、有多个子值或下一次状态依赖前一次状态的场景。 |
| Reducer 函数 | (state, action) => newState | 纯函数,根据 action 类型返回新状态。 | 类似 Redux,但内置于 React。 |
代码示例:
import React, { useReducer } from 'react';
// 定义 reducer
function counterReducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: action.payload };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset', payload: 0 })}>Reset</button>
</>
);
}
export default Counter;
11.4 useCallback Hook
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
useCallback | const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]); | 缓存函数实例,避免在每次渲染时创建新函数。 | 用于传递给子组件的回调函数,配合 React.memo 优化性能。 |
| 避免不必要的重渲染 | 当子组件使用 React.memo 时,父组件传递的函数若每次都变,会失效。 | 依赖数组必须包含所有在回调中使用的值。 | — |
代码示例:
import React, { useState, useCallback, memo } from 'react';
// 子组件使用 React.memo 优化
const ExpensiveComponent = memo(({ onClick, value }) => {
console.log('ExpensiveComponent rendered');
return <button onClick={onClick}>Count: {value}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [otherState, setOtherState] = useState('');
// ✅ 使用 useCallback 缓存函数
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []); // 无依赖
return (
<div>
<input value={otherState} onChange={e => setOtherState(e.target.value)} />
<ExpensiveComponent onClick={handleClick} value={count} />
</div>
);
}
export default Parent;
11.5 useMemo Hook
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
useMemo | const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); | 缓存计算结果,避免在每次渲染时执行昂贵的计算。 | 用于性能优化;不要用于副作用。 |
| 优化计算 | 如排序、过滤大型数组、复杂数学运算。 | 依赖数组变化时重新计算;不保证一定缓存(可能在下次渲染时重新计算)。 | — |
代码示例:
import React, { useState, useMemo } from 'react';
function SlowComponent({ list, filterText }) {
// ✅ 使用 useMemo 缓存过滤结果
const filteredList = useMemo(() => {
console.log('Filtering list...');
return list.filter(item =>
item.toLowerCase().includes(filterText.toLowerCase())
);
}, [list, filterText]);
return (
<ul>
{filteredList.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
);
}
function App() {
const [filter, setFilter] = useState('');
const largeList = Array.from({ length: 1000 }, (_, i) => `Item ${i}`);
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} placeholder="Filter" />
<SlowComponent list={largeList} filterText={filter} />
</div>
);
}
export default App;
11.6 自定义 Hook 的创建与使用
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 自定义 Hook | function useCustomHook() { ... return value; } | 封装可复用的逻辑(如状态逻辑、副作用),以 use 开头命名。 | 可以调用其他 Hook;必须在函数组件或其他自定义 Hook 中调用。 |
| 逻辑复用 | 将跨组件的逻辑提取为独立函数,提高代码复用性和可维护性。 | 返回值可以是任意类型(状态、函数、对象等)。 | — |
代码示例:创建并使用自定义 Hook
import React, { useState, useEffect } from 'react';
// 自定义 Hook:管理本地存储的状态
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// 同步到 localStorage
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
// 使用自定义 Hook
function UserPreferences() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [fontSize, setFontSize] = useLocalStorage('fontSize', 16);
return (
<div style={{ fontSize }}>
<p>Current theme: {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
<button onClick={() => setFontSize(fontSize + 2)}>Increase Font</button>
</div>
);
}
export default UserPreferences;
第十二章:Context API
12.1 什么是 Context
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| Context | React.createContext(defaultValue) | 提供一种在组件树中传递数据的方式,避免”props 逐层传递”(prop drilling)。 | 适用于主题、用户认证、语言、UI 状态等全局或跨层级数据。 |
| 数据传递 | 跨越中间组件直接向深层后代传递数据。不需要将 props 通过每一层组件手动传递。 | 不应滥用,仅用于真正需要全局访问的数据。 | — |
✅ 核心思想:Context 允许你”广播”数据给整个组件树,任何后代组件都可以选择”订阅”这些数据。
12.2 创建 Context
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
createContext | const MyContext = React.createContext(defaultValue); | 创建一个 Context 对象。 | defaultValue 是当组件没有匹配的 Provider 时使用的值(可用于测试或独立使用)。 |
| Context 对象 | 包含 .Provider 和 .Consumer 两个属性。用于后续提供和消费上下文值。 | 通常将 Context 导出为模块,供其他组件使用。 | — |
代码示例:
import React from 'react';
// 创建一个主题上下文,初始值为 'light'
const ThemeContext = React.createContext('light');
// 创建一个用户上下文,初始值为 null
const UserContext = React.createContext(null);
// 导出 Context,供其他组件导入使用
export { ThemeContext, UserContext };
12.3 Provider 与 Consumer 的使用
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
Provider | <MyContext.Provider value={value}>...</MyContext.Provider> | 提供上下文的值,其后代组件可访问该值。 | value 可以是任意类型(字符串、对象、函数等)。 |
Consumer | <MyContext.Consumer>{value => <div>{value}</div>}</MyContext.Consumer> | 通过 render props 模式消费上下文值。 | 语法较繁琐,现代开发推荐使用 useContext。 |
代码示例:
import React from 'react';
import { ThemeContext } from './context/ThemeContext';
function App() {
const [theme, setTheme] = React.useState('dark');
return (
// 使用 Provider 提供主题值
<ThemeContext.Provider value={theme}>
<Toolbar onToggle={() => setTheme(theme === 'light' ? 'dark' : 'light')} />
</ThemeContext.Provider>
);
}
function Toolbar({ onToggle }) {
return (
<div>
<ThemedButton />
<button onClick={onToggle}>Toggle Theme</button>
</div>
);
}
// 使用 Consumer 消费主题值
function ThemedButton() {
return (
<ThemeContext.Consumer>
{currentTheme => (
<button style={{ background: currentTheme === 'dark' ? '#333' : '#fff', color: currentTheme === 'dark' ? '#fff' : '#000' }}>
I am {currentTheme}
</button>
)}
</ThemeContext.Consumer>
);
}
export default App;
12.4 使用 useContext 消费 Context
| 名称 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
useContext | const value = useContext(MyContext); | 在函数组件中直接读取上下文的当前值。 | 必须先导入创建的 Context 对象;组件会在上下文值变化时重新渲染。 |
| 简化消费 | 替代 Consumer,语法更简洁,易于理解。是现代 React 中消费 Context 的首选方式。 | 不能在类组件中使用(类组件使用 static contextType 或 Consumer)。 | — |
代码示例(优化上例):
import React, { useContext } from 'react';
import { ThemeContext } from './context/ThemeContext';
function App() {
const [theme, setTheme] = React.useState('dark');
return (
<ThemeContext.Provider value={theme}>
<Toolbar onToggle={() => setTheme(theme === 'light' ? 'dark' : 'light')} />
</ThemeContext.Provider>
);
}
function Toolbar({ onToggle }) {
return (
<div>
<ThemedButton />
<button onClick={onToggle}>Toggle Theme</button>
</div>
);
}
// 使用 useContext 消费主题值(更简洁)
function ThemedButton() {
const theme = useContext(ThemeContext); // ✅ 推荐
return (
<button style={{ background: theme === 'dark' ? '#333' : '#fff', color: theme === 'dark' ? '#fff' : '#000' }}>
I am {theme}
</button>
);
}
export default App;
12.5 Context 的性能优化
| 问题 | 解决方案 | 说明 |
|---|---|---|
| Provider 值变化导致所有后代重新渲染 | 将 value 对象拆分为多个 Context,或使用 useMemo 缓存 value。 | React 会重新渲染所有消费该 Context 的组件,即使它们只关心部分数据。 |
| 避免不必要的 Provider 重渲染 | 将 Provider 放在稳定组件中,或使用 React.memo 包装父组件。 | 如果 Provider 的父组件频繁更新,可能导致不必要的 Context 更新。 |
使用 useMemo 缓存 value | <MyContext.Provider value={useMemo(() => ({ state, dispatch }), [state])}> | 当 value 是对象或函数时,防止每次渲染都创建新引用。 |
代码示例:性能优化
import React, { useState, useMemo, useContext } from 'react';
// 拆分 Context 避免过度渲染
const ThemeContext = React.createContext();
const UserContext = React.createContext();
function App() {
const [theme, setTheme] = useState('dark');
const [user, setUser] = useState({ name: 'Alice' });
// ✅ 使用 useMemo 缓存 value,防止每次渲染都创建新对象
const themeValue = useMemo(() => ({ theme, setTheme }), [theme, setTheme]);
const userValue = useMemo(() => ({ user, setUser }), [user, setUser]);
return (
<ThemeContext.Provider value={themeValue}>
<UserContext.Provider value={userValue}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
function ThemeButton() {
const { theme, setTheme } = useContext(ThemeContext);
// 只有 theme 变化时才会重新渲染
return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>{theme}</button>;
}
function UserGreeting() {
const { user } = useContext(UserContext);
// 只有 user 变化时才会重新渲染
return <p>Hello, {user.name}!</p>;
}
12.6 使用场景与替代方案(如状态管理库)
| 场景/方案 | 说明 | 建议 |
|---|---|---|
| 适用场景 | 主题切换 | ✅ 推荐使用 Context |
| 用户认证状态 | ||
| 多语言(i18n) | ||
| UI 状态(如侧边栏展开) | ||
| 跨多层组件的配置 | ||
| 不适用场景 | 高频更新的状态(如每秒多次) | ❌ 应考虑状态管理库 |
| 大型复杂状态树 | ||
| 需要时间旅行调试、中间件等高级功能 | ||
| 替代方案 | Redux:功能强大,适合大型应用,有丰富的中间件生态。 | 根据项目复杂度选择 |
| Zustand:轻量、简洁,基于 Hooks,易于上手。 | ||
| Jotai / Recoil:原子化状态管理,与 React 模型更契合。 |
总结:
- Context + useReducer 可以满足大多数中等复杂度应用的状态管理需求。
- 对于大型或复杂应用,建议结合状态管理库(如 Redux Toolkit、Zustand)以获得更好的开发体验和性能。
第十三章:高阶组件(HOC)
13.1 HOC 的概念与作用
| 名称 | 语法/模式 | 用途 | 注意事项 |
|---|---|---|---|
| 高阶组件(HOC) | const EnhancedComponent = hoc(WrappedComponent); | 一个函数,接收一个组件并返回一个新组件。 | 不是 React API 的一部分,而是一种模式。 |
| 作用 | 复用组件逻辑(如数据获取、权限检查) | 提高代码复用性和可维护性。 | HOC 不应修改原组件,而应通过组合方式增强。 |
| 抽象通用行为(如加载状态、错误处理) | |||
| 修改组件行为或注入 props |
✅ 核心思想:HOC 类似于高阶函数,用于”包装”组件,为其添加额外功能。
13.2 编写一个 HOC
| 名称 | 语法/模式 | 用途 | 注意事项 |
|---|---|---|---|
| 基本结构 | function withHOC(WrappedComponent) { return function EnhancedComponent(props) { return <WrappedComponent {...props} />; }; } | 创建一个返回新组件的函数。 | 新组件通常称为”增强组件”(Enhanced Component)。 |
| 注入 props | 在返回的组件中传递额外的 props。例如注入用户信息、主题、API 数据等。 | 避免 props 冲突,可使用命名空间或 ...rest。 | — |
代码示例:创建一个日志 HOC
import React from 'react';
// HOC:记录组件的挂载和卸载
function withLogger(WrappedComponent) {
return class extends React.Component {
componentDidMount() {
console.log(`Component ${WrappedComponent.name} mounted`);
}
componentWillUnmount() {
console.log(`Component ${WrappedComponent.name} unmounted`);
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
// 使用 HOC
function UserProfile({ name }) {
return <div>User: {name}</div>;
}
const LoggedUserProfile = withLogger(UserProfile);
function App() {
const [show, setShow] = React.useState(true);
return (
<div>
<button onClick={() => setShow(!show)}>
{show ? 'Hide' : 'Show'} Profile
</button>
{show && <LoggedUserProfile name="Alice" />}
</div>
);
}
export default App;
13.3 属性代理与反向继承
| 模式 | 语法/说明 | 用途 | 注意事项 |
|---|---|---|---|
| 属性代理(Props Proxy) | return <WrappedComponent {...this.props} extraProp={value} />; | 控制传入被包装组件的 props,可读取、修改、添加或删除 props。 | 最常用模式;适用于大多数场景(如注入、转换 props)。 |
| 反向继承(Inheritance Inversion) | class HOC extends WrappedComponent { render() { return super.render(); } } | 返回的组件继承自被包装组件,可访问其 state、props、生命周期等。 | 可操作被包装组件的内部实现,但破坏封装性,不推荐。 |
代码示例:属性代理 vs 反向继承
import React from 'react';
// 1. 属性代理:添加 loading 状态
function withLoading(WrappedComponent) {
return function LoadingHOC({ isLoading, ...props }) {
if (isLoading) {
return <div>Loading...</div>;
}
// ✅ 属性代理:传递剩余 props
return <WrappedComponent {...props} />;
};
}
// 2. 反向继承:修改渲染逻辑(不推荐)
function withConditionalRendering(WrappedComponent) {
return class extends WrappedComponent {
render() {
// 可访问 super.render(),但侵入性强
const element = super.render();
if (this.props.condition === false) {
return null;
}
return element;
}
};
}
// 使用
function DataComponent({ data }) {
return <div>Data: {data}</div>;
}
const LoadingData = withLoading(DataComponent);
const ConditionalData = withConditionalRendering(DataComponent);
function App() {
return (
<div>
<LoadingData isLoading={true} />
<ConditionalData condition={false} data="Hello" />
</div>
);
}
export default App;
13.4 HOC 的组合
| 名称 | 语法/模式 | 用途 | 注意事项 |
|---|---|---|---|
| 组合函数 | compose(f, g, h)(x) 或 h(g(f(x))) | 将多个 HOC 组合应用到一个组件上。 | 避免嵌套过深,提高可读性。 |
| 函数组合工具 | 手写 compose 或使用 Lodash 的 flowRight。从右到左依次应用 HOC。 | React 生态中常见(如 Redux 的 connect)。 | — |
代码示例:组合多个 HOC
import React from 'react';
// 多个 HOC
function withAuth(WrappedComponent) {
return function AuthHOC(props) {
const [isLoggedIn] = React.useState(true);
return isLoggedIn ? <WrappedComponent {...props} /> : <div>Please login</div>;
};
}
function withTheme(WrappedComponent) {
return function ThemeHOC(props) {
const theme = 'dark';
return <WrappedComponent {...props} theme={theme} />;
};
}
function withLogging(WrappedComponent) {
return function LoggingHOC(props) {
React.useEffect(() => {
console.log('Component rendered');
});
return <WrappedComponent {...props} />;
};
}
// 组合 HOC
function compose(...funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
// 应用多个 HOC
const enhance = compose(
withLogging,
withTheme,
withAuth
);
function Profile({ name, theme }) {
return <div style={{ color: theme === 'dark' ? '#fff' : '#000' }}>Profile: {name}</div>;
}
const EnhancedProfile = enhance(Profile);
function App() {
return <EnhancedProfile name="Bob" />;
}
export default App;
13.5 HOC 的局限性与 Hooks 的对比
| 问题/对比 | 说明 | 建议 |
|---|---|---|
| Wrapper Hell | 多个 HOC 嵌套导致 JSX 层级过深,调试困难。 | 使用 Hooks 可避免组件嵌套。 |
| 命名冲突 | HOC 注入的 props 可能与组件原有 props 冲突。 | 使用命名空间或 Hooks 避免。 |
| 静态方法丢失 | HOC 返回的新组件不继承原组件的静态方法。 | 需手动复制,或使用 hoist-non-react-statics 库。 |
| Refs 不被传递 | Refs 不能穿透 HOC,需使用 React.forwardRef。 | Hooks 天然支持 ref。 |
| 与 Hooks 对比 | Hooks:逻辑复用更简洁,无嵌套,支持组合 | ✅ 现代 React 推荐优先使用 Hooks,HOC 作为补充。 |
| HOC:适用于类组件或需要修改组件结构的场景 |
总结:
- HOC 是 React 类组件时代的逻辑复用方案,功能强大但存在嵌套和冲突问题。
- Hooks 是 React 函数组件时代的首选,更简洁、直观,推荐用于新项目。
- 何时使用 HOC:需要操作组件生命周期、修改渲染树、或在不支持 Hooks 的旧代码中。
第十四章:React Router 基础
14.1 安装与配置 React Router
| 名称 | 语法/用法 | 用途 | 注意事项 |
|---|---|---|---|
| 安装 | npm install react-router-dom | 安装 React Router 的 DOM 版本(用于 Web 应用)。 | 确保项目已安装 react 和 react-dom。 |
| 配置 | 在应用最外层包裹 <BrowserRouter> | 启用客户端路由,监听 URL 变化并更新 UI。 | 通常在 index.js 或 App.js 的最顶层使用。 |
代码示例:项目入口配置
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
✅ BrowserRouter 使用 HTML5 的 history.pushState API 实现 URL 导航,无需页面刷新。
14.2 路由组件:BrowserRouter、Route、Link
| 组件 | 语法/用法 | 用途 | 注意事项 |
|---|---|---|---|
BrowserRouter | <BrowserRouter><App /></BrowserRouter> | 提供路由能力,管理浏览器历史记录。 | 必须是路由相关组件的祖先。 |
Routes | <Routes><Route path="..." element={<Component />} /></Routes> | 包含一组 Route,匹配并渲染第一个匹配的路由。 | v6 中取代了 v5 的 Switch。 |
Route | <Route path="/" element={<Home />} /> | 定义路径与要渲染组件的映射。 | 必须放在 Routes 内部;使用 element 属性(非 component)。 |
Link | <Link to="/about">About</Link> | 创建可点击的导航链接,点击后不刷新页面。 | 替代原生 <a> 标签,防止页面跳转。 |
代码示例:基础路由结构
// App.js
import React from 'react';
import { Routes, Route, Link } from 'react-router-dom';
function Home() {
return <h2>Home Page</h2>;
}
function About() {
return <h2>About Us</h2>;
}
function Contact() {
return <h2>Contact Info</h2>;
}
function App() {
return (
<div>
<nav>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/contact">Contact</Link></li>
</ul>
</nav>
{/* 路由出口 */}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</div>
);
}
export default App;
14.3 动态路由与参数传递
| 名称 | 语法/用法 | 用途 | 注意事项 |
|---|---|---|---|
| 动态段 | :paramName(如 /users/:id) | 匹配动态路径片段。 | : 后为参数名。 |
useParams | const { id } = useParams(); | 在组件中读取动态路由参数。 | 必须在 Route 渲染的组件中使用。 |
代码示例:用户详情页
import React from 'react';
import { Routes, Route, Link, useParams } from 'react-router-dom';
function Users() {
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
];
return (
<div>
<h2>Users</h2>
<ul>
{users.map(user => (
<li key={user.id}>
<Link to={`/users/${user.id}`}>{user.name}</Link>
</li>
))}
</ul>
{/* 嵌套路由出口 */}
<Routes>
<Route path=":id" element={<UserProfile />} />
</Routes>
</div>
);
}
function UserProfile() {
const { id } = useParams(); // 获取动态参数
return <p>User ID: {id}</p>;
}
function App() {
return (
<Routes>
<Route path="/" element={<div><h1>Home</h1><Link to="/users">View Users</Link></div>} />
<Route path="/users/*" element={<Users />} />
</Routes>
);
}
export default App;
14.4 嵌套路由
| 名称 | 语法/用法 | 用途 | 注意事项 |
|---|---|---|---|
| 嵌套路由 | 在父组件内部使用 <Routes> 和 path=":id" | 实现父子页面结构(如用户列表 → 用户详情)。 | 父路由路径以 /* 结尾,或使用 end={false}(v6.4+)。 |
| 相对路径 | Link 中使用相对路径(如 to="edit") | 在嵌套上下文中导航。 | 相对路径基于当前路由。 |
代码示例:管理用户
import React from 'react';
import { Routes, Route, Link, useParams, useMatch } from 'react-router-dom';
function UserDashboard() {
const { id } = useParams();
return (
<div>
<h3>User {id} Dashboard</h3>
<nav>
<Link to="profile">Profile</Link> |
<Link to="settings">Settings</Link>
</nav>
<Routes>
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Routes>
</div>
);
}
function Profile() {
return <p>Profile Page</p>;
}
function Settings() {
return <p>Settings Page</p>;
}
function UsersList() {
const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
return (
<div>
<h2>Users</h2>
<ul>
{users.map(user => (
<li key={user.id}>
<Link to={`${user.id}/profile`}>User {user.name}</Link>
</li>
))}
</ul>
<Routes>
<Route path=":id/*" element={<UserDashboard />} />
</Routes>
</div>
);
}
function App() {
return (
<Routes>
<Route path="/" element={<h1>Home</h1>} />
<Route path="/users/*" element={<UsersList />} />
</Routes>
);
}
export default App;
14.5 编程式导航(useNavigate)
| 名称 | 语法/用法 | 用途 | 注意事项 |
|---|---|---|---|
useNavigate | const navigate = useNavigate(); | 在函数组件中通过 JavaScript 控制导航。 | 取代 v5 的 history.push。 |
| 跳转 | navigate('/target') | 导航到指定路径。 | 可传递 state:navigate('/page', { state: { from: 'home' } })。 |
| 相对导航 | navigate('relative-path') | 在嵌套路由中跳转。 | 基于当前路径。 |
| 回退 | navigate(-1) | 返回上一页。 | 类似浏览器后退按钮。 |
代码示例:登录后跳转
import React, { useState } from 'react';
import { Routes, Route, Link, useNavigate } from 'react-router-dom';
function Login() {
const [username, setUsername] = useState('');
const navigate = useNavigate();
const handleSubmit = (e) => {
e.preventDefault();
// 模拟登录成功
console.log('Logged in as', username);
// 跳转到主页
navigate('/', { replace: true }); // replace 替换当前历史记录
};
return (
<form onSubmit={handleSubmit}>
<h2>Login</h2>
<input
value={username}
onChange={e => setUsername(e.target.value)}
placeholder="Username"
required
/>
<button type="submit">Login</button>
</form>
);
}
function Home() {
return (
<div>
<h2>Welcome!</h2>
<Link to="/login">Go to Login</Link>
</div>
);
}
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
</Routes>
);
}
export default App;
第十五章:性能优化
15.1 使用 React.memo 进行组件记忆
| 名称 | 语法/用法 | 用途 | 注意事项 |
|---|---|---|---|
React.memo | const MyComponent = React.memo(function MyComponent(props) { ... }); | 高阶组件,缓存组件的渲染输出,当 props 不变时跳过重新渲染。 | 仅适用于函数组件;默认进行浅比较,可传入自定义比较函数。 |
| 自定义比较 | React.memo(Component, (prevProps, nextProps) => {...}) | 精确控制是否跳过渲染。 | 返回 true 表示跳过渲染,false 表示重新渲染。 |
代码示例:避免不必要的子组件渲染
import React, { useState } from 'react';
// ✅ 使用 React.memo 优化
const ExpensiveComponent = React.memo(({ data }) => {
console.log('ExpensiveComponent rendered'); // 仅在 data 变化时打印
return <div>Processed: {data.toUpperCase()}</div>;
});
function Parent() {
const [count, setCount] = useState(0);
const [input, setInput] = useState('');
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Type something"
/>
{/* 只有 input 变化时,ExpensiveComponent 才会重新渲染 */}
<ExpensiveComponent data={input} />
</div>
);
}
export default Parent;
15.2 使用 useMemo 缓存计算结果
| 名称 | 语法/用法 | 用途 | 注意事项 |
|---|---|---|---|
useMemo | const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); | 缓存昂贵的计算结果,避免在每次渲染时重复执行。 | 用于性能优化;不要用于副作用。 |
| 适用场景 | 排序、过滤大型数组、复杂数学运算、对象创建等。 | 依赖数组必须包含所有在回调中使用的变量。 | — |
代码示例:缓存过滤结果
import React, { useState, useMemo } from 'react';
function FilteredList({ items, filter }) {
// ✅ 使用 useMemo 缓存过滤结果
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item => item.includes(filter));
}, [items, filter]); // 依赖项
return (
<ul>
{filteredItems.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
);
}
function App() {
const [filterText, setFilterText] = useState('');
const largeList = Array.from({ length: 1000 }, (_, i) => `Item ${i}`);
return (
<div>
<input
value={filterText}
onChange={e => setFilterText(e.target.value)}
placeholder="Filter items"
/>
<FilteredList items={largeList} filter={filterText} />
</div>
);
}
export default App;
15.3 使用 useCallback 缓存函数
| 名称 | 语法/用法 | 用途 | 注意事项 |
|---|---|---|---|
useCallback | const memoizedFn = useCallback(fn, deps); | 缓存函数实例,避免在每次渲染时创建新函数。 | 主要用于传递给子组件的回调函数,配合 React.memo 使用。 |
| 避免重渲染 | 当子组件使用 React.memo 时,若父组件传递的函数每次都变,会导致优化失效。 | 依赖数组必须包含函数中使用的所有变量。 | — |
代码示例:配合 React.memo 优化
import React, { useState, useCallback, memo } from 'react';
const Child = memo(({ onClick, value }) => {
console.log('Child rendered');
return <button onClick={onClick}>Count: {value}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// ✅ 使用 useCallback 缓存函数
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []); // 无依赖
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<Child onClick={handleClick} value={count} />
</div>
);
}
export default Parent;
15.4 虚拟化长列表(React Virtualized)
| 名称 | 语法/用法 | 用途 | 注意事项 |
|---|---|---|---|
| 虚拟化 | 只渲染可视区域内的元素,大幅减少 DOM 节点。解决长列表(如 10000+ 项)导致的性能问题。 | 需要固定或可预测的行高。 | — |
| 推荐库 | react-window(轻量)、react-virtualized(功能全) | 提供 FixedSizeList、VariableSizeList 等组件。 | 安装:npm install react-window |
代码示例:使用 react-window 虚拟化列表
import React from 'react';
import { FixedSizeList as List } from 'react-window';
// 每行渲染的组件
const Row = ({ index, style }) => (
<div style={style}>
Item {index}
</div>
);
function VirtualizedList() {
const itemCount = 10000; // 模拟大量数据
return (
<List
height={400} // 容器高度
itemCount={itemCount}
itemSize={35} // 每行高度
width="100%"
>
{Row}
</List>
);
}
function App() {
return (
<div>
<h2>Virtualized Long List</h2>
<VirtualizedList />
</div>
);
}
export default App;
⚠️ 需先安装:npm install react-window
15.5 使用 Profiler 进行性能分析
| 名称 | 语法/用法 | 用途 | 注意事项 |
|---|---|---|---|
<Profiler> | <Profiler id="..." onRender={callback}>...</Profiler> | 测量组件树的渲染性能,识别慢速渲染。 | 仅在开发模式下工作;生产环境无开销。 |
onRender 回调 | (id, phase, actualDuration, baseDuration, ...) | 获取性能数据。 | actualDuration:本次渲染耗时;baseDuration:估算不使用 memo 的耗时。 |
代码示例:分析组件渲染性能
import React, { Profiler, useState } from 'react';
// 模拟慢速组件
function SlowComponent({ iterations }) {
// 模拟昂贵计算
const start = performance.now();
while (performance.now() - start < 10) {} // 延迟 10ms
return <div>Slow Component ({iterations} iterations)</div>;
}
// 性能分析回调
function onRenderCallback(
id, // 发生提交的 Profiler 树的 "id"
phase, // "mount" 或 "update"
actualDuration, // 本次更新 committed 花费的毫秒数
baseDuration, // 估算重新渲染整棵子树需要的总毫秒数
startTime, // 本次更新开始的时间
commitTime, // 本次更新 committed 的时间
interactions // 本次更新关联的 interactions
) {
console.log({
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
});
}
function App() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Performance Profiler</h1>
{/* 包裹需要分析的组件 */}
<Profiler id="SlowComponent" onRender={onRenderCallback}>
<SlowComponent iterations={count} />
</Profiler>
<button onClick={() => setCount(count + 1)}>
Rerender Slow Component (Count: {count})
</button>
</div>
);
}
export default App;