React 教程

React 是一个用于构建用户界面(UI)的 JavaScript 库,由 Facebook(Meta)开发并开源。它用"组件化 + 声明式"的方式构建界面,是目前最流行的前端框架之一。

React 的核心思想

  • 组件化:UI 由独立、可复用的组件构成
  • 声明式:描述"UI 应该长什么样",React 负责更新
  • 单向数据流:数据从父组件流向子组件
  • 虚拟 DOM:高效更新真实 DOM

一个 React 组件

function Welcome({ name }) {
  return <h1>你好,{name}!</h1>;
}

function App() {
  return (
    <div>
      <Welcome name="CodeNest" />
      <Welcome name="React" />
    </div>
  );
}

React 的生态

领域常用方案
构建工具Vite、Next.js、Create React App
路由React Router
状态管理Redux、Zustand、Context
UI 组件库Ant Design、Material UI、Tailwind
服务端渲染Next.js、Remix

谁在用 React

  • Facebook / Instagram / WhatsApp
  • Netflix、Airbnb、Uber
  • 知乎、小红书等国内大厂

React 与 Vue 的选择

对比项ReactVue
学习曲线JSX 较陡模板较平缓
生态庞大完善
灵活度高度灵活约定更多
💡 提示

React 是"库"而非完整框架,路由、状态管理等由社区方案补齐;这也是它灵活的原因。

React 简介

深入了解 React 的核心概念:组件、JSX、Props、State,建立心智模型。

React 的哲学:声明式 UI

传统命令式开发(jQuery 时代):"先找到元素,再改内容"。React 声明式:"描述 UI 状态,React 自动更新"。

// 命令式(不推荐)
const el = document.getElementById('count');
el.textContent = count;

// 声明式(React 方式)
function Counter() {
  const [count, setCount] = useState(0);
  return <p>当前:{count}</p>;
}

五个核心概念

概念说明
组件函数或类,返回 UI 片段
JSX在 JS 中写 HTML 的语法扩展
Props父组件传给子组件的数据(只读)
State组件内部可变的数据
Hooks函数组件中使用状态和副作用

组件树的渲染

App
├── Header
├── ArticleList
│   ├── ArticleCard
│   └── ArticleCard
└── Footer

数据自顶向下流动(单向),事件自底向上传递。

React 的版本演进

  • React 16.8(2019):Hooks 引入,函数组件崛起
  • React 18(2022):并发渲染、自动批处理
  • React 19:Actions、编译器优化

什么时候用 React

  • 复杂交互的 SPA(单页应用)
  • 组件复用度高的大型前端
  • 需要生态支持的团队项目
💡 提示

先理解"组件 + 数据流",比背 API 更重要——遇到问题都能从这两个概念推导出答案。

React 安装(NPM)

通过 NPM 安装 React 并搭建开发环境。

环境要求

  • Node.js 18+(node -v 查看)
  • npm(随 Node 一起安装)

方式一:使用 Create React App(CRA)

# 创建项目(较慢,已不推荐新项目使用)
npx create-react-app my-app
cd my-app
npm start

方式二:Vite(推荐)

npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev

方式三:手动安装(理解原理)

mkdir react-demo && cd react-demo
npm init -y
npm install react react-dom

# 开发依赖
npm install -D vite @vitejs/plugin-react

验证安装

node -v        # v22.x
npm -v         # 10.x

项目结构(Vite)

my-app/
├── index.html        # 入口 HTML
├── src/
│   ├── main.jsx      # 应用入口
│   ├── App.jsx       # 根组件
│   └── index.css
├── package.json
└── vite.config.js

常见问题

问题解决
npm 下载慢配置淘宝镜像:npm config set registry https://registry.npmmirror.com
Node 版本过低用 nvm 安装新版 Node
端口被占用npm run dev -- --port 3001
💡 提示

新项目推荐 Vite:秒级启动、热更新快;CRA 已停止维护,不建议新项目使用。

React 安装(Vite)

使用 Vite 快速创建 React 项目——当前社区推荐的标配方案。

创建项目

npm create vite@latest my-react-app -- --template react

# 或者用类型模板
npm create vite@latest my-react-app -- --template react-ts

安装与启动

cd my-react-app
npm install
npm run dev      # 启动开发服务器(默认 5173 端口)

构建生产版本

npm run build    # 输出到 dist/
npm run preview  # 本地预览构建结果

Vite 项目结构

my-react-app/
├── index.html          # 入口(含 <div id="root">)
├── vite.config.js      # Vite 配置
├── src/
│   ├── main.jsx        # 入口:ReactDOM.createRoot
│   ├── App.jsx         # 根组件
│   ├── App.css
│   └── assets/         # 静态资源
└── public/             # 公共文件

入口文件 main.jsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';

// 挂载到 root 节点
ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

vite.config.js 常用配置

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,              // 自定义端口
    proxy: {                 // 开发代理
      '/api': 'http://localhost:8000',
    },
  },
});
💡 提示

server.proxy 让开发时前端请求 /api 自动转发到后端(如 FastAPI 8000 端口),解决开发跨域。

React 安装(CDN)

通过 CDN 快速体验 React——无需安装任何东西,一个 HTML 文件即可运行。

最简 CDN 示例

<!DOCTYPE html>
<html>
<body>
  <div id="root"></div>

  <!-- React CDN -->
  <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>

  <!-- Babel 编译 JSX -->
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

  <script type="text/babel">
    function App() {
      return <h1>Hello, React CDN!</h1>;
    }
    ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>

CDN 方式的特点

优点缺点
零安装,双击即用无打包优化,性能一般
适合学习/演示不适合生产大型应用
可用于 Codepen 等在线平台依赖 CDN 可用性

在线体验平台

  • CodeSandbox:完整 React 环境
  • CodePen:快速片段实验
  • StackBlitz:Vite 在线开发
  • PlayCode:轻量在线 IDE

什么时候用 CDN

  • 快速验证一个想法
  • 写教程/演示代码
  • 小型落地页(不推荐,用原生更好)
⚠️ 注意

生产环境不要用 CDN + Babel 方式(体积大、无优化);用 Vite 构建才是正道。

React VSCode

在 VS Code 中配置 React 开发环境,获得智能提示与高效开发体验。

1. 安装扩展

  • ES7+ React/Redux/React-Native snippets:代码片段(输入 rfce 快速生成组件)
  • Prettier:代码格式化
  • ESLint:代码检查
  • Auto Import:自动导入

2. 设置 JSX 高亮

VS Code 默认支持 .jsx/.tsx 文件;对 .js 文件启用 JSX:

// settings.json
{
  "files.associations": {
    "*.js": "javascriptreact"
  }
}

3. 配置 Emmet(快速输入)

{
  "emmet.includeLanguages": {
    "javascript": "javascriptreact"
  }
}

输入 div.container>h1+p 按 Tab 自动展开为 JSX 结构。

4. Prettier 配置

// .prettierrc
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5"
}

5. 调试配置

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "launch",
      "name": "Debug React",
      "url": "http://localhost:5173",
      "webRoot": "${workspaceFolder}/src"
    }
  ]
}

6. 常用快捷键

快捷键作用
rfce + Tab生成函数组件
rafce + Tab生成箭头函数组件
Cmd/Ctrl + D选中下一个相同词
Alt + ↑/↓移动行
💡 提示

React 开发者工具(浏览器扩展)是调试必备:可查看组件树、Props、State、性能分析。

React Qoder

使用 Qoder(AI 编程工具)辅助 React 开发,让 AI 帮你写代码、改 bug。

Qoder 的核心能力

  • 代码生成:自然语言描述 → React 组件
  • 智能补全:上下文感知的代码提示
  • Bug 修复:报错信息直接提问
  • 重构建议:优化组件结构

示例:让 AI 生成组件

用户输入:
"帮我写一个 React 计数器组件,支持加、减、重置,显示当前值"

AI 输出:
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>当前值:{count}</p>
      <button => setCount(count + 1)}>+1</button>
      <button => setCount(count - 1)}>-1</button>
      <button => setCount(0)}>重置</button>
    </div>
  );
}

AI 辅助开发的注意点

  • 生成代码要理解后再用,不要无脑复制
  • 关键逻辑自己把关(状态、副作用)
  • 给 AI 清晰的上下文(组件结构、技术栈)
  • 让 AI 解释代码,学习效果更好

推荐提示词模板

1. "用 React + Vite 写一个 XX 组件,要求:..."
2. "这段代码为什么报错:<粘贴代码和报错>"
3. "把这段 JSX 重构为可复用组件"
4. "这个 useEffect 有什么问题?如何改进?"
💡 提示

AI 工具是放大器:基础扎实的人用 AI 效率翻倍,基础薄弱的人容易被带偏。先学原理,再用工具。

React 在线生成

使用在线生成工具快速创建 React 应用/组件,无需本地环境。

在线沙箱平台

平台特点
CodeSandbox功能完整,支持 Vite/Next,可分享
StackBlitz浏览器内运行 Vite,速度极快
CodePen轻量,适合单个组件/效果演示
Replit支持全栈(前后端一起)

CodeSandbox 使用流程

  1. 打开 codesandbox.io
  2. 选择模板:React / React+TS / Vite
  3. 在线编写代码,实时预览
  4. 一键分享链接或导入 GitHub

生成 React 应用的在线工具

# 在浏览器中体验(无需安装)
1. 打开 StackBlitz → New Project → React
2. 模板已含 Vite + React,直接写代码
3. 支持导入 npm 包

AI 生成 UI 工具

  • v0.dev(Vercel):文字描述 → UI 组件
  • Builder.io:AI 生成组件代码
  • Figma → React:设计稿转组件

什么时候用在线工具

  • 临时实验、快速原型
  • 面试演示、代码分享
  • 没有本地环境的场景
💡 提示

在线沙箱适合学习与演示;正式项目仍建议本地 Vite 开发,配合 Git 管理版本。

React 创建第一个项目

创建你的第一个 React 项目,从零理解 React 应用的启动流程。

快速创建

npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev

打开 http://localhost:5173 看到 Vite + React 欢迎页即成功。

清空模板,写自己的代码

// src/App.jsx
function App() {
  return (
    <div>
      <h1>我的第一个 React 应用</h1>
      <p>欢迎来到 CodeNest React 教程!</p>
    </div>
  );
}

export default App;

理解启动流程

  1. index.html 加载 /src/main.jsx
  2. main.jsx 用 createRoot 挂载 <App /> 到 #root
  3. App.jsx 返回的 JSX 被编译渲染为页面
  4. Vite 热更新:改代码保存,页面自动刷新

运行三种模式

npm run dev       # 开发:热更新
npm run build     # 构建:输出 dist/
npm run preview   # 预览构建产物

检查 React 是否运行

浏览器按 F12 → Console,React 开发工具扩展会显示组件树;修改 App.jsx 内容保存,页面应实时变化。

💡 提示

开发模式(dev)带热更新和错误提示,构建模式(build)才是生产用的压缩产物。

React 项目说明

解析 Vite + React 项目的每个文件,理解它们各自的作用。

关键文件详解

文件作用
index.html页面入口,含 <div id="root"> 挂载点
src/main.jsx应用入口,创建根节点并渲染 App
src/App.jsx根组件,组合所有页面/组件
src/App.css根组件样式
src/index.css全局样式
vite.config.jsVite 构建配置(插件、代理)
package.json依赖与脚本(dev/build/preview)
public/静态资源(原样拷贝)

index.html 要点

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <title>My App</title>
</head>
<body>
  <!-- React 挂载点 -->
  <div id="root"></div>
  <script type="module" src="/src/main.jsx"></script>
</body>
</html>

package.json 脚本

{
  "scripts": {
    "dev": "vite",               // 开发
    "build": "vite build",       // 构建
    "preview": "vite preview",   // 预览
    "lint": "eslint ."           // 检查
  },
  "dependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}

组件组织建议

src/
├── components/     # 通用组件
│   ├── Button.jsx
│   └── Card.jsx
├── pages/          # 页面组件
│   ├── Home.jsx
│   └── About.jsx
├── hooks/          # 自定义 Hook
│   └── useAuth.js
├── api/            # 接口请求
│   └── client.js
├── utils/          # 工具函数
│   └── format.js
└── App.jsx
💡 提示

项目变复杂后,按"组件/页面/Hook/接口/工具"分类组织文件,代码可维护性大幅提升。

React 元素渲染

理解 React 的元素渲染机制:元素、渲染过程、更新逻辑。

React 元素

React 元素是描述 UI 的不可变对象,通过 JSX 创建:

const element = <h1>Hello, World!</h1>;

// 编译后等价于:
// const element = React.createElement('h1', null, 'Hello, World!');

渲染到 DOM

import ReactDOM from 'react-dom/client';

const root = ReactDOM.createRoot(document.getElementById('root'));

// 渲染
root.render(<App />);

更新已渲染的元素

// 元素不可变,更新 = 重新渲染
function tick() {
  const element = (
    <div>
      <h1>当前时间</h1>
      <p>{new Date().toLocaleTimeString()}</p>
    </div>
  );
  root.render(element);
}

setInterval(tick, 1000);   // 每秒重新渲染

React 只更新变化的部分

React 通过虚拟 DOM diff 对比前后差异,只更新变化节点——即使每秒重新渲染整个元素,也只改动时间文本。

渲染规则

规则说明
元素不可变创建后不能修改其属性/子元素
更新靠重新渲染用新元素替换旧元素
最小化 DOM 操作diff 后只更新差异部分

实际开发中

日常开发很少直接操作 root.render()——通常由 useState 触发组件重新渲染。理解元素机制有助于理解 React 的更新模型。

💡 提示

"元素不可变 + 重新渲染"是 React 的核心心智:状态变了 → 重新调用组件函数 → 得到新 UI 描述 → 高效更新 DOM。

React JSX

JSX 是 React 的语法糖:在 JavaScript 中写类似 HTML 的结构,编译后变成 React.createElement 调用。

为什么用 JSX

  • 直观:UI 结构一目了然
  • 安全:默认防 XSS 注入
  • 表达力强:可嵌入 JS 表达式

JSX 嵌入表达式

const name = 'CodeNest';
const user = { name: '张三', age: 25 };

function Greeting() {
  return (
    <div>
      <h1>你好,{name}!</h1>           {/* 变量 */}
      <p>年龄:{user.age + 1}</p>      {/* 表达式 */}
      <p>长度:{"hello".length}</p>    {/* 函数调用 */}
    </div>
  );
}

JSX 属性

const imgUrl = 'https://example.com/logo.png';

// 属性用驼峰命名(className 而非 class)
<div className="card" id="main">
  <img src={imgUrl} alt="Logo" />
  <button disabled={false}>点击</button>
</div>

JSX 子元素

// 嵌套
<div>
  <h1>标题</h1>
  <p>内容</p>
</div>

// 数组渲染
const items = ['A', 'B', 'C'];
<ul>
  {items.map(item => <li key={item}>{item}</li>)}
</ul>

JSX 条件渲染

function Status({ isLoggedIn }) {
  return (
    <div>
      {isLoggedIn ? <p>已登录</p> : <p>未登录</p>}
      {/* 短路渲染 */}
      {isLoggedIn && <button>退出</button>}
    </div>
  );
}

JSX 注意事项

注意点说明
必须包裹多个元素要包在 <div> 或 <Fragment> 中
classNameclass 是保留字,用 className
自闭合空标签要写 <img />、<br />
注释JSX 内注释用 {/* ... */}
💡 提示

JSX 不是模板语言,就是 JavaScript——可以随意使用表达式、函数、逻辑运算。

React 组件

组件是 React 的核心构建单元。理解函数组件与类组件,掌握组件的组合方式。

函数组件(推荐)

// 定义
function Welcome({ name }) {
  return <h1>你好,{name}</h1>;
}

// 使用
function App() {
  return <Welcome name="CodeNest" />;
}

箭头函数组件

const Welcome = ({ name }) => (
  <h1>你好,{name}</h1>
);

export default Welcome;

组件命名规则

  • 组件名大写字母开头(PascalCase)
  • 文件名与组件名一致:Welcome.jsx
  • React 区分组件(大写)与 HTML 标签(小写)

组件的组合

function Avatar({ user }) {
  return <img src={user.avatar} alt={user.name} />;
}

function UserInfo({ user }) {
  return (
    <div className="user-info">
      <Avatar user={user} />
      <span>{user.name}</span>
    </div>
  );
}

function App() {
  const user = { name: '张三', avatar: '/a.png' };
  return <UserInfo user={user} />;
}

组件 vs 实例

同一个组件可以渲染多个实例,各自独立:

function App() {
  return (
    <div>
      <Counter />
      <Counter />    {/* 独立实例,互不影响 */}
      <Counter />
    </div>
  );
}

组件设计原则

  • 单一职责:一个组件只做一件事
  • 可复用:通过 Props 配置差异
  • 组件树:大组件拆分为小组件组合
💡 提示

把页面想象成"积木":先搭大积木(页面),再用小积木(组件)填充细节,每个积木可单独复用。

React 组件状态

组件状态(State)是组件内部可变的数据,通过 useState Hook 管理。

useState 基础

import { useState } from 'react';

function Counter() {
  // count 是当前值,setCount 是更新函数
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>点击了 {count} 次</p>
      <button => setCount(count + 1)}>
        点击
      </button>
    </div>
  );
}

更新状态的正确姿势

function Counter() {
  const [count, setCount] = useState(0);

  // 推荐:函数式更新(基于旧值)
  const increment = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);   // 连加两次也正确
  };

  // 避免:直接依赖旧值(多次调用有问题)
  // const increment = () => setCount(count + 1);

  return <button
}

更新对象状态

function Profile() {
  const [user, setUser] = useState({ name: '张三', age: 25 });

  // 必须创建新对象(不可变更新)
  const updateName = () => {
    setUser(prev => ({ ...prev, name: '李四' }));
  };

  return (
    <div>
      <p>{user.name} - {user.age}岁</p>
      <button
    </div>
  );
}

更新数组状态

function TodoList() {
  const [todos, setTodos] = useState(['学习 React']);

  const addTodo = () => {
    setTodos(prev => [...prev, '新任务']);    // 追加
  };

  const removeTodo = (index) => {
    setTodos(prev => prev.filter((_, i) => i !== index));  // 删除
  };

  return (
    <div>
      <ul>{todos.map((t, i) => <li key={i}>{t}</li>)}</ul>
      <button
    </div>
  );
}

State 的要点

要点说明
不可变永远创建新值,不直接修改旧值
异步更新setState 后不能立刻读新值(用函数式更新)
触发重渲染状态变化 → 组件重新渲染
💡 提示

Props 是"外部传入",State 是"内部自有"——两者的区别是 React 数据流的核心。

React Props

Props(属性)是父组件传给子组件的数据,在子组件中只读。

传递 Props

function Card({ title, content, author }) {
  return (
    <div className="card">
      <h3>{title}</h3>
      <p>{content}</p>
      <small>作者:{author}</small>
    </div>
  );
}

function App() {
  return (
    <div>
      <Card title="React 入门" content="..." author="老王" />
      <Card title="JSX 详解" content="..." author="老王" />
    </div>
  );
}

Props 类型

function Product({ id, name, price, inStock, tags, onBuy }) {
  return (
    <div>
      <p>{id} - {name}:¥{price}</p>
      <p>{inStock ? '有货' : '缺货'},标签:{tags.join(', ')}</p>
      <button => onBuy(id)}>购买</button>
    </div>
  );
}

// 传各种类型
<Product
  id={1}
  name="键盘"
  price={299.0}
  inStock={true}
  tags={['外设', '办公']}
  => console.log('购买', id)}
/>

默认 Props

function Greeting({ name = '游客', age = 0 }) {
  return <p>{name}({age}岁)</p>;
}

// 或
Greeting.defaultProps = { name: '游客' };

children(子元素)

function Card({ children, title }) {
  return (
    <div className="card">
      <h3>{title}</h3>
      {children}   {/* 组件内部的内容 */}
    </div>
  );
}

<Card title="提示">
  <p>这里是卡片内容</p>
</Card>

Props 单向流动

数据从父组件流向子组件,子组件不能修改 Props。要改数据,应该在父组件中通过状态管理:

function App() {
  const [count, setCount] = useState(0);
  return (
    <Counter
      count={count}
      => setCount(count + 1)}
    />
  );
}
💡 提示

"Props 向下传,事件向上报"是 React 的数据流铁律——父组件管状态,子组件管展示。

React 事件处理

React 的事件处理:绑定事件、传递参数、合成事件。

绑定事件

function Button() {
  const handleClick = () => {
    console.log('点击了!');
  };

  return <button
}

内联写法

<button => console.log('click')}>内联</button>

事件对象

function Input() {
  const handleChange = (e) => {
    console.log(e.target.value);    // 输入的值
    console.log(e.target.name);     // 元素 name
  };

  return <input name="username" />;
}

传递参数

function List({ items }) {
  const handleDelete = (id) => {
    console.log('删除', id);
  };

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          {item.name}
          <button => handleDelete(item.id)}>
            删除
          </button>
        </li>
      ))}
    </ul>
  );
}

阻止默认行为

function Form() {
  const handleSubmit = (e) => {
    e.preventDefault();    // 阻止表单刷新
    console.log('提交!');
  };

  return (
    <form
      <button type="submit">提交</button>
    </form>
  );
}

常见事件

事件触发时机
onClick点击
onChange输入框内容变化
onSubmit表单提交
onMouseEnter鼠标移入
onKeyDown按键按下
onFocus / onBlur聚焦/失焦
💡 提示

React 事件是"合成事件"(统一封装),写法用驼峰(onClick),且事件委托在根节点,性能好。

React 条件渲染

条件渲染:根据条件显示不同 UI 的四种常用方式。

1. if / else 语句

function Status({ isLoggedIn }) {
  if (isLoggedIn) {
    return <p>欢迎回来!</p>;
  }
  return <p>请先登录</p>;
}

2. 三元运算符

function Status({ isLoggedIn }) {
  return (
    <div>
      {isLoggedIn ? (
        <button>退出登录</button>
      ) : (
        <button>登录</button>
      )}
    </div>
  );
}

3. 逻辑与(短路)

function Notification({ count }) {
  return (
    <div>
      {count > 0 && <p>你有 {count} 条新消息</p>}
      {/* count <= 0 时渲染 false(什么都不显示) */}
    </div>
  );
}

4. 逻辑或(默认值)

function Name({ user }) {
  return <p>{user.name || '匿名用户'}</p>;
}

阻止渲染(返回 null)

function WarningBanner({ show }) {
  if (!show) {
    return null;     // 不渲染任何内容
  }
  return <div className="warning">警告!</div>;
}

结合状态的条件渲染

function App() {
  const [page, setPage] = useState('home');

  return (
    <div>
      <nav>
        <button => setPage('home')}>首页</button>
        <button => setPage('about')}>关于</button>
      </nav>

      {page === 'home' && <Home />}
      {page === 'about' && <About />}
    </div>
  );
}
💡 提示

多个分支用 if/else 或提前 return;简单二选一用三元;只显示/隐藏用 &&。

React 列表 & Keys

渲染列表与理解 Keys 的重要性——React 列表渲染的核心。

渲染列表

function ArticleList({ articles }) {
  return (
    <ul>
      {articles.map(article => (
        <li key={article.id}>
          <h3>{article.title}</h3>
          <p>{article.summary}</p>
        </li>
      ))}
    </ul>
  );
}

为什么需要 key

key 帮助 React 识别哪些列表项被增删改,从而高效更新 DOM。没有 key(或用 index)在增删列表项时可能引发渲染错误。

// ✅ 用唯一 id
{items.map(item => <Item key={item.id} item={item} />)}

// ⚠️ 避免用 index(列表变化时可能错乱)
{items.map((item, index) => <Item key={index} item={item} />)}

key 的选择规则

key适用
数据库 id首选(稳定唯一)
slug/URL有语义的唯一值
index静态列表、无需重排时勉强可用

列表筛选

function FilteredList({ todos, filter }) {
  const visible = todos.filter(todo => {
    if (filter === 'active') return !todo.done;
    if (filter === 'done') return todo.done;
    return true;
  });

  return (
    <ul>
      {visible.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

提取列表项组件

function ArticleCard({ article, onSelect }) {
  return (
    <article => onSelect(article.id)}>
      <h3>{article.title}</h3>
      <p>{article.summary}</p>
    </article>
  );
}

// key 放在 map 的组件上
{articles.map(a => (
  <ArticleCard key={a.id} article={a} />
))}
💡 提示

key 只需在同层兄弟节点间唯一;不要在组件内部自己生成 key(index 随机数都不可靠)。

React 组件 API

组件 API:React 组件对外暴露的属性和方法,主要是 Props 与 State 的规范使用。

组件的两种数据

类型来源可变用途
Props父组件传入❌ 只读配置组件
State组件自身✅ 可变管理内部数据

Props 的规范使用

function UserCard({ user, onEdit, showEmail = true }) {
  return (
    <div>
      <p>{user.name}</p>
      {showEmail && <p>{user.email}</p>}
      <button => onEdit(user.id)}>编辑</button>
    </div>
  );
}

State 提升(共享状态)

多个子组件需要共享状态时,把状态提升到共同的父组件:

function App() {
  const [selectedId, setSelectedId] = useState(null);

  return (
    <div>
      <List />
      <Detail id={selectedId} />   {/* 两个组件共享状态 */}
    </div>
  );
}

受控组件(表单)

function ControlledInput() {
  const [value, setValue] = useState('');

  // 值由 React 状态控制 → 受控组件
  return (
    <input
      value={value}
      => setValue(e.target.value)}
    />
  );
}

非受控组件

function UncontrolledInput() {
  // 值由 DOM 自己管理,用 ref 读取
  const inputRef = useRef(null);

  const readValue = () => {
    console.log(inputRef.current.value);
  };

  return (
    <div>
      <input ref={inputRef} defaultValue="初始值" />
      <button
    </div>
  );
}

组件通信总结

场景方案
父 → 子Props
子 → 父回调函数(Props 传函数)
兄弟组件状态提升到父组件
深层/全局Context 或状态库(Redux/Zustand)
💡 提示

大部分场景用"Props 向下 + 回调向上"即可解决;别一上来就上状态库,先想清楚层级关系。

React 组件生命周期

组件生命周期:组件从挂载到卸载的过程。函数组件用 useEffect 实现生命周期逻辑。

生命周期的三个阶段

阶段时机函数组件对应
挂载组件第一次渲染useEffect(..., [])
更新Props/State 变化useEffect(..., [deps])
卸载组件从 DOM 移除useEffect 清理函数

挂载时执行一次

import { useEffect, useState } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // 组件挂载时请求数据
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(setUser);
  }, [userId]);      // 依赖变化才重新执行

  return user ? <p>{user.name}</p> : <p>加载中...</p>;
}

依赖数组的三种写法

// 1. 空数组:只挂载时执行一次
useEffect(() => { load(); }, []);

// 2. 带依赖:依赖变化时执行
useEffect(() => { load(id); }, [id]);

// 3. 不传:每次渲染都执行(慎用)
useEffect(() => { console.log('每次渲染'); });

卸载时清理

useEffect(() => {
  const timer = setInterval(() => {
    console.log('定时器运行中');
  }, 1000);

  // 清理函数:组件卸载时执行
  return () => clearInterval(timer);
}, []);

订阅/取消订阅

useEffect(() => {
  // 订阅事件
  window.addEventListener('resize', handleResize);

  // 卸载时取消订阅(防止内存泄漏)
  return () => window.removeEventListener('resize', handleResize);
}, []);

React 18 StrictMode 注意

开发模式下 StrictMode 会双调用 effect(挂载→卸载→挂载),用于暴露清理函数缺失的问题——生产环境无影响。

💡 提示

把"数据请求、订阅、计时器"等副作用统一放 useEffect,并记得在清理函数中回收资源。

React AJAX

在 React 中发送 AJAX 请求:请求时机、Loading 状态、错误处理。

用 useEffect 发请求

import { useEffect, useState } from 'react';

function ArticleList() {
  const [articles, setArticles] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;    // 防止卸载后 setState

    fetch('/api/articles')
      .then(r => {
        if (!r.ok) throw new Error('请求失败');
        return r.json();
      })
      .then(data => {
        if (!cancelled) {
          setArticles(data.items);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err.message);
          setLoading(false);
        }
      });

    return () => { cancelled = true; };
  }, []);

  if (loading) return <p>加载中...</p>;
  if (error) return <p>出错了:{error}</p>;

  return (
    <ul>
      {articles.map(a => <li key={a.id}>{a.title}</li>)}
    </ul>
  );
}

封装请求 Hook

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);

    fetch(url)
      .then(r => r.json())
      .then(d => { if (!cancelled) { setData(d); setLoading(false); } })
      .catch(e => { if (!cancelled) { setError(e); setLoading(false); } });

    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

// 使用
function App() {
  const { data, loading, error } = useFetch('/api/articles');
  // ...
}

POST 请求

async function createArticle(formData) {
  const res = await fetch('/api/articles', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(formData),
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(err.message || '创建失败');
  }
  return res.json();
}

请求库 axios

import axios from 'axios';

async function getArticles() {
  const res = await axios.get('/api/articles', {
    params: { page: 1, page_size: 10 },
  });
  return res.data;   // 自动解析 JSON
}

Loading 与错误处理最佳实践

  • 三个状态:loading / data / error
  • 卸载后不要 setState(用 cancelled 标志)
  • 错误要展示给用户,而不是静默失败
  • 重复请求可加防抖或取消(AbortController)
💡 提示

React 18 + Suspense 未来可简化数据请求;现在社区常用 TanStack Query(react-query)管理服务端状态。

React 表单与事件

React 表单与事件:受控表单、输入处理、表单提交的完整实践。

受控表单(推荐)

function LoginForm() {
  const [form, setForm] = useState({ username: '', password: '' });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm(prev => ({ ...prev, [name]: value }));   // 动态更新字段
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('提交:', form);
    // 发送请求...
  };

  return (
    <form
      <input
        name="username"
        value={form.username}
       
        placeholder="用户名"
      />
      <input
        type="password"
        name="password"
        value={form.password}
       
        placeholder="密码"
      />
      <button type="submit">登录</button>
    </form>
  );
}

复选框与下拉

function Settings() {
  const [prefs, setPrefs] = useState({ dark: false, lang: 'zh' });

  return (
    <div>
      <label>
        <input
          type="checkbox"
          checked={prefs.dark}
          => setPrefs({ ...prefs, dark: e.target.checked })}
        />
        暗色模式
      </label>

      <select
        value={prefs.lang}
        => setPrefs({ ...prefs, lang: e.target.value })}
      >
        <option value="zh">中文</option>
        <option value="en">English</option>
      </select>
    </div>
  );
}

多选与文件

// 多选
const handleTags = (e) => {
  const options = [...e.target.selectedOptions];
  const values = options.map(o => o.value);
  setForm(f => ({ ...f, tags: values }));
};

// 文件
const handleFile = (e) => {
  setForm(f => ({ ...f, file: e.target.files[0] }));
};

表单校验

function ValidatedForm() {
  const [errors, setErrors] = useState({});

  const validate = (form) => {
    const err = {};
    if (form.username.length < 3) err.username = '用户名至少 3 个字符';
    if (!/^\S+@\S+\.\S+$/.test(form.email)) err.email = '邮箱格式不正确';
    if (form.password.length < 6) err.password = '密码至少 6 位';
    return err;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const err = validate(form);
    if (Object.keys(err).length > 0) {
      setErrors(err);
      return;
    }
    // 提交...
  };

  return (
    <form
      <input name="username" value={form.username} />
      {errors.username && <p className="error">{errors.username}</p>}
      {/* ... */}
    </form>
  );
}
💡 提示

复杂表单可用库:Formik / React Hook Form(性能好)+ zod/yup 校验,能省大量样板代码。

React Refs

Refs:访问 DOM 元素或保存跨渲染的数据,不触发重新渲染。

访问 DOM 元素

import { useRef, useEffect } from 'react';

function FocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();    // 挂载后自动聚焦
  }, []);

  return <input ref={inputRef} placeholder="自动聚焦" />;
}

测量元素尺寸

function MeasureBox() {
  const boxRef = useRef(null);

  const measure = () => {
    const el = boxRef.current;
    console.log(el.getBoundingClientRect());   // 位置尺寸
  };

  return (
    <div>
      <div ref={boxRef} className="box">测量我</div>
      <button
    </div>
  );
}

保存可变值(不触发渲染)

function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null);   // 保存定时器 id

  const start = () => {
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
  };

  const stop = () => {
    clearInterval(intervalRef.current);
  };

  return (
    <div>
      <p>{count} 秒</p>
      <button
      <button
    </div>
  );
}

useRef vs useState

对比项useRefuseState
变化触发渲染❌ 不触发✅ 触发
用途DOM 访问、保存值UI 数据
读取ref.currentstate 变量

forwardRef(透传 ref)

import { forwardRef } from 'react';

// 让父组件能访问子组件的 DOM
const CustomInput = forwardRef(function CustomInput(props, ref) {
  return <input ref={ref} {...props} />;
});

function Parent() {
  const inputRef = useRef(null);
  return (
    <div>
      <CustomInput ref={inputRef} />
      <button => inputRef.current.focus()}>聚焦</button>
    </div>
  );
}
💡 提示

能用 state 表达 UI 就用 state;需要"命令式"操作 DOM(聚焦、滚动、测量)时才用 ref。

React 条件判断

条件判断进阶:复杂条件渲染、切换组件、列表条件过滤。

多条件分支

function StatusBadge({ status }) {
  if (status === 'pending') return <span className="badge yellow">待处理</span>;
  if (status === 'processing') return <span className="badge blue">处理中</span>;
  if (status === 'done') return <span className="badge green">已完成</span>;
  if (status === 'failed') return <span className="badge red">失败</span>;
  return <span>未知状态</span>;
}

用对象映射(优雅)

const STATUS_MAP = {
  pending: { label: '待处理', cls: 'yellow' },
  processing: { label: '处理中', cls: 'blue' },
  done: { label: '已完成', cls: 'green' },
  failed: { label: '失败', cls: 'red' },
};

function StatusBadge({ status }) {
  const conf = STATUS_MAP[status] || { label: '未知', cls: 'gray' };
  return <span className={`badge ${conf.cls}`}>{conf.label}</span>;
}

切换不同组件

function Page({ type }) {
  switch (type) {
    case 'home': return <Home />;
    case 'about': return <About />;
    case 'contact': return <Contact />;
    default: return <NotFound />;
  }
}

条件渲染与列表组合

function Dashboard({ todos }) {
  const doneCount = todos.filter(t => t.done).length;

  return (
    <div>
      <p>进度:{doneCount}/{todos.length}</p>

      {todos.length === 0 ? (
        <p className="empty">暂无任务 🎉</p>
      ) : (
        <ul>
          {todos.map(todo => (
            <li key={todo.id} className={todo.done ? 'done' : ''}>
              {todo.text}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

防抖搜索(条件 + 定时器)

function SearchBox({ onSearch }) {
  const [keyword, setKeyword] = useState('');

  useEffect(() => {
    // 输入停止 300ms 后才搜索
    const timer = setTimeout(() => onSearch(keyword), 300);
    return () => clearTimeout(timer);
  }, [keyword]);

  return (
    <input
      value={keyword}
      => setKeyword(e.target.value)}
      placeholder="搜索..."
    />
  );
}
💡 提示

条件分支多时,优先"对象映射"或"switch",比堆叠三元表达式可读性强得多。

React 路由

React Router:实现多页面应用(SPA)的路由跳转。

安装

npm install react-router-dom

基本路由配置

import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">首页</Link>
        <Link to="/about">关于</Link>
        <Link to="/articles">文章</Link>
      </nav>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/articles" element={<ArticleList />} />
        <Route path="/articles/:id" element={<ArticleDetail />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

Link 与 useNavigate

import { Link, useNavigate } from 'react-router-dom';

// 声明式导航
<Link to="/articles/42">查看文章 42</Link>

// 编程式导航
function LoginButton() {
  const navigate = useNavigate();

  const handleLogin = () => {
    // 登录成功后跳转
    navigate('/dashboard', { state: { from: 'login' } });
  };

  return <button
}

路由参数

import { useParams } from 'react-router-dom';

function ArticleDetail() {
  const { id } = useParams();       // 读取 :id

  useEffect(() => {
    fetch(`/api/articles/${id}`).then(...);
  }, [id]);

  return <div>文章 {id}</div>;
}

查询参数

import { useSearchParams } from 'react-router-dom';

function SearchPage() {
  const [searchParams, setSearchParams] = useSearchParams();
  const keyword = searchParams.get('q') || '';

  const updateKeyword = (value) => {
    setSearchParams({ q: value });   // URL 变为 ?q=xxx
  };

  return (
    <input
      value={keyword}
      => updateKeyword(e.target.value)}
    />
  );
}

嵌套路由与 Outlet

import { Outlet } from 'react-router-dom';

function Layout() {
  return (
    <div>
      <Header />
      <main>
        <Outlet />     {/* 子路由渲染位置 */}
      </main>
      <Footer />
    </div>
  );
}

// 配置
<Route path="/" element={<Layout />}>
  <Route index element={<Home />} />
  <Route path="about" element={<About />} />
</Route>
💡 提示

用 Link 而非 <a> 做站内跳转(避免整页刷新);需要操作后用 useNavigate。

React Memo

React.memo:优化组件渲染性能,避免不必要的重复渲染。

为什么需要 memo

父组件重新渲染时,子组件默认也会重新渲染。如果子组件计算昂贵(或 props 没变),这就是浪费。

import { memo, useState } from 'react';

// 普通组件:父组件每次渲染都会重新渲染
function Expensive({ data }) {
  console.log('渲染 Expensive');
  return <div>{data.title}</div>;
}

// memo 组件:props 没变时不重新渲染
const MemoExpensive = memo(function Expensive({ data }) {
  console.log('渲染 MemoExpensive(props 变了才渲染)');
  return <div>{data.title}</div>;
});

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button => setCount(count + 1)}>{count}</button>
      <MemoExpensive data={{ title: '固定数据' }} />
    </div>
  );
}

memo 的注意事项

  • memo 是浅比较 props:对象/数组每次都是新引用,memo 失效
  • 搭配 useCallback / useMemo 保持引用稳定
  • 不是所有组件都需要 memo,先测性能再优化

useCallback(缓存函数)

import { useCallback, memo } from 'react';

const Button = memo(function Button({ onClick, label }) {
  console.log('渲染 Button');
  return <button
});

function App() {
  const [count, setCount] = useState(0);

  // 缓存函数:count 不变时引用不变 → Button 不重渲染
  const handleClick = useCallback(() => {
    console.log('点击');
  }, []);

  return (
    <div>
      <p>{count}</p>
      <Button label="按钮" />
    </div>
  );
}

useMemo(缓存计算结果)

import { useMemo } from 'react';

function Stats({ list }) {
  // 计算昂贵:只有 list 变化才重新计算
  const total = useMemo(() => {
    return list.reduce((sum, item) => sum + item.price, 0);
  }, [list]);

  return <p>总价:{total}</p>;
}

何时使用

工具场景
memo组件渲染昂贵且 props 常不变
useCallback传给 memo 子组件的函数
useMemo计算昂贵的派生数据
💡 提示

性能优化原则:先测量,再优化。盲目加 memo 反而可能因浅比较开销拖慢性能。

React CSS

在 React 中使用 CSS:全局样式、CSS Modules、CSS-in-JS 三种主流方式。

1. 全局 CSS(最基础)

// App.css
.container { max-width: 1200px; margin: 0 auto; }
.card { border: 1px solid #eee; border-radius: 8px; padding: 16px; }
// 组件中引入
import './App.css';

function Card() {
  return <div className="card">卡片内容</div>;
}

2. CSS Modules(推荐,作用域隔离)

// Card.module.css
.card { border: 1px solid #eee; }
.title { font-size: 18px; color: #333; }
// Card.jsx
import styles from './Card.module.css';

function Card() {
  return (
    <div className={styles.card}>
      <h3 className={styles.title}>标题</h3>
    </div>
  );
}

CSS Modules 会自动生成唯一类名(如 Card_card_x1y2z),避免样式冲突。

3. CSS-in-JS(styled-components)

npm install styled-components
import styled from 'styled-components';

const Button = styled.button`
  background: #4f46e5;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 6px;
  cursor: pointer;

  &:hover {
    background: #4338ca;
  }

  ${props => props.primary && css`
    font-size: 18px;
    font-weight: bold;
  `}
`;

function App() {
  return (
    <div>
      <Button>默认按钮</Button>
      <Button primary>主按钮</Button>
    </div>
  );
}

4. 内联样式

function InlineStyle() {
  const style = {
    color: '#fff',
    background: '#4f46e5',
    padding: '8px 16px',
    borderRadius: 4,
  };
  return <button style={style}>内联样式</button>;
}

方案对比

方案作用域动态样式适用
全局 CSS全局弱小项目
CSS Modules组件级中中大型(推荐)
styled-components组件级强需要主题定制
Tailwind原子类强快速开发(见后续章节)
💡 提示

新项目推荐 CSS Modules 或 Tailwind;styled-components 适合组件库/主题化场景。

React Sass

在 React 中使用 Sass(SCSS):变量、嵌套、混入等增强 CSS 的能力。

安装 Sass

npm install -D sass

Vite 无需额外配置,直接支持 .scss 文件。

Sass 变量

// styles/_variables.scss
$primary: #4f46e5;
$danger: #ef4444;
$radius: 8px;
$spacing: 16px;
// Card.scss
@use './variables';

.card {
  border: 1px solid #eee;
  border-radius: variables.$radius;
  padding: variables.$spacing;

  .title {
    color: variables.$primary;
  }
}

嵌套语法

.nav {
  display: flex;

  ul { list-style: none; }

  li {
    margin: 0 8px;

    &:hover {          // & 引用父选择器
      color: $primary;
    }
  }

  a {
    text-decoration: none;

    &::after {
      content: '→';
    }
  }
}

混入(Mixin)

@mixin flex-center {
  display: flex;
  align-items: center;
  justify-content: center;
}

@mixin button-style($bg, $color: white) {
  background: $bg;
  color: $color;
  padding: 10px 20px;
  border: none;
  border-radius: 6px;
}

.btn-primary {
  @include button-style($primary);
}

.btn-danger {
  @include button-style($danger);
}

.loading {
  @include flex-center;
}

在 React 中使用

// Card.scss
.card {
  padding: 16px;
  border: 1px solid #ddd;
}

// Card.jsx
import './Card.scss';

function Card({ children }) {
  return <div className="card">{children}</div>;
}

Sass 与 CSS Modules 结合

// Card.module.scss
.card { ... }

// Card.jsx
import styles from './Card.module.scss';

function Card() {
  return <div className={styles.card}>...</div>;
}
💡 提示

现代 CSS 已有变量(var())和嵌套支持,小项目可以直接用原生 CSS;Sass 更适合复杂设计系统。

React Tailwind CSS

在 React 中使用 Tailwind CSS:原子化 CSS 框架,快速构建界面。

安装 Tailwind

npm install -D tailwindcss @tailwindcss/vite
// vite.config.js
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [react(), tailwindcss()],
});
/* index.css */
@import "tailwindcss";

原子类示例

function Card() {
  return (
    <div className="max-w-sm mx-auto bg-white rounded-xl shadow-md p-6">
      <h3 className="text-xl font-bold text-gray-800 mb-2">标题</h3>
      <p className="text-gray-600 mb-4">内容描述...</p>
      <button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">
        按钮
      </button>
    </div>
  );
}

常用类速查

类别示例
布局flex items-center justify-between gap-4
间距p-4 m-2 mt-6 mb-4 px-3
文字text-lg font-bold text-red-500 text-center
背景bg-blue-500 bg-gradient-to-r
圆角阴影rounded-lg shadow-md border
响应式md:flex lg:grid-cols-3
状态hover:bg-blue-700 focus:outline-none

条件样式

function Button({ variant = 'primary' }) {
  const base = 'px-4 py-2 rounded-lg font-medium';
  const styles = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700',
    danger: 'bg-red-500 text-white hover:bg-red-600',
    ghost: 'bg-transparent text-gray-700 hover:bg-gray-100',
  };

  return <button className={`${base} ${styles[variant]}`}>按钮</button>;
}

Tailwind vs 传统 CSS

对比项Tailwind传统 CSS
开发速度快(免起类名)中
样式体积按需生成,很小取决于写多少
学习成本类名体系CSS 本身
可维护性组件内聚样式表集中
💡 提示

Tailwind 与组件化天生契合:样式写在组件里,删组件即删样式,不产生死代码。

React Hooks

React Hooks:函数组件中使用状态与副作用的神器。本节系统梳理常用 Hooks。

Hooks 总览

Hook用途
useState组件状态
useEffect副作用(请求/订阅/定时器)
useContext读取 Context 值
useRefDOM 引用 / 保存可变值
useMemo缓存计算结果
useCallback缓存函数
useReducer复杂状态管理
useLayoutEffect同步副作用(测量布局)

useContext 跨层传值

import { createContext, useContext, useState } from 'react';

// 1. 创建 Context
const ThemeContext = createContext('light');

// 2. Provider 提供值
function App() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

// 3. 任意层组件消费
function Toolbar() {
  const { theme, setTheme } = useContext(ThemeContext);
  return (
    <button => setTheme(theme === 'light' ? 'dark' : 'light')}>
      当前:{theme},点击切换
    </button>
  );
}

useReducer 复杂状态

import { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'add': return [...state, action.todo];
    case 'toggle':
      return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
    case 'remove':
      return state.filter(t => t.id !== action.id);
    default: return state;
  }
}

function TodoApp() {
  const [todos, dispatch] = useReducer(reducer, []);

  return (
    <div>
      <button => dispatch({ type: 'add', todo: { id: Date.now(), text: '任务' } })}>
        添加
      </button>
      {todos.map(t => (
        <div key={t.id}>
          <span => dispatch({ type: 'toggle', id: t.id })}>
            {t.done ? '✅' : '⬜'} {t.text}
          </span>
        </div>
      ))}
    </div>
  );
}

自定义 Hook

// useWindowWidth.js
import { useState, useEffect } from 'react';

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handler = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);

  return width;
}

// 使用(任何组件都能用)
function Layout() {
  const width = useWindowWidth();
  return <p>窗口宽度:{width}px</p>;
}

Hooks 使用规则

  1. 只在组件顶层调用(不在循环/条件/嵌套函数中)
  2. 只在函数组件或自定义 Hook中调用
  3. 自定义 Hook 命名以 use 开头
💡 提示

Hook 的核心价值:逻辑复用。把可复用的逻辑抽成自定义 Hook,组件只负责 UI。

React 参考手册

整理 React 常用参考:API 速查、资源链接、常见问题。

常用 API 速查

API作用
ReactDOM.createRoot(el).render(vdom)挂载应用
useState(init)返回 [state, setState]
useEffect(fn, deps)副作用
useRef(init)可变引用
useContext(Ctx)读取 Context
memo(Comp)组件记忆化
lazy(() => import())代码分割懒加载
Suspense懒加载占位

代码分割(懒加载)

import { lazy, Suspense } from 'react';

// 路由级懒加载
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

学习资源

资源链接/说明
React 官方文档react.dev(新版,含交互式教程)
React Routerreactrouter.com
Ant Designant.design(企业级组件库)
Tailwind CSStailwindcss.com
Next.jsnextjs.org(React 全栈框架)

常见问题

问题解决
Hooks 报错 "Invalid hook call"检查是否在条件/循环中调用
状态更新不生效确认用了 setState 而非直接赋值
列表渲染错乱检查 key 是否稳定唯一
组件不更新检查是否修改了对象/数组(要不可变更新)
警告 "useEffect deps"按提示补全依赖或使用 eslint-plugin-react-hooks
💡 提示

遇到报错先读控制台完整信息;React 的错误提示非常友好,通常直接告诉你原因和修法。

React 测验

本章测验检验你对 React 的掌握程度。先独立作答,再看解析。

选择题

1. React 中创建组件最推荐的方式?

  • A. 函数组件 + Hooks ✅
  • B. 类组件
  • C. HTML 模板
  • D. jQuery 插件

2. 下列哪个是更新状态的正确写法?

  • A. count = count + 1
  • B. this.count++
  • C. setCount(count + 1) ✅
  • D. count.value++

3. 父组件传给子组件的数据叫什么?

  • A. State
  • B. Props ✅
  • C. Ref
  • D. Store

4. 列表渲染时元素需要什么属性?

  • A. id
  • B. name
  • C. key ✅
  • D. ref

5. 哪个 Hook 用于处理副作用(请求/定时器)?

  • A. useState
  • B. useEffect ✅
  • C. useRef
  • D. useMemo

6. JSX 中 class 应写成?

  • A. class
  • B. className ✅
  • C. class-name
  • D. cssClass

判断题

7. Props 在子组件中可以被修改。

❌ 错误。Props 是只读的,修改数据应通过父组件的 State。

8. useEffect 的依赖数组为空时,只在组件挂载后执行一次。

✅ 正确。[] 表示"没有依赖",只在首次挂载后执行。

简答题

9. 写一个计数器组件(useState 版)。

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button => setCount(count + 1)}>
      {count}
    </button>
  );
}

10. 如何让列表渲染正确且高效?

{items.map(item => (
  <Item key={item.id} data={item} />
))}

要点:稳定唯一的 key(用 id 而非 index)。

计分

答对水平
9-10优秀,可开发真实项目
6-8良好,巩固薄弱点
<6建议重学基础章节
💡 提示

React 重在实践:写完测验后,试着用 React 重做一个小项目(如待办清单),收获最大。

React 博客项目

综合运用 React 知识,构建一个完整的博客项目(前后端分离模式)。

项目规划

功能:
1. 文章列表(分页、分类筛选)
2. 文章详情
3. 搜索
4. 路由跳转
5. 主题切换(暗色/亮色)
6. 打包部署

项目结构

my-blog/
├── src/
│   ├── components/        # 通用组件
│   │   ├── Header.jsx
│   │   ├── ArticleCard.jsx
│   │   └── Pagination.jsx
│   ├── pages/             # 页面
│   │   ├── Home.jsx
│   │   ├── ArticleDetail.jsx
│   │   └── Search.jsx
│   ├── api/               # 接口
│   │   └── client.js
│   ├── hooks/             # 自定义 Hook
│   │   └── useFetch.js
│   ├── App.jsx
│   └── main.jsx
├── index.html
└── package.json

API 封装

// src/api/client.js
const BASE = '/api';

async function request(path, options = {}) {
  const res = await fetch(`${BASE}${path}`, {
    headers: { 'Content-Type': 'application/json' },
    ...options,
  });
  if (!res.ok) throw new Error(`请求失败:${res.status}`);
  return res.json();
}

export const getArticles = (params) => request(`/articles?${new URLSearchParams(params)}`);
export const getArticle = (id) => request(`/articles/${id}`);
export const searchArticles = (q) => request(`/search?q=${encodeURIComponent(q)}`);

文章列表页

// src/pages/Home.jsx
import { useEffect, useState } from 'react';
import { getArticles } from '../api/client';
import ArticleCard from '../components/ArticleCard';

function Home() {
  const [articles, setArticles] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    getArticles({ page: 1, page_size: 10 })
      .then(data => setArticles(data.items))
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <p>加载中...</p>;

  return (
    <div className="grid">
      {articles.map(a => <ArticleCard key={a.id} article={a} />)}
    </div>
  );
}

export default Home;

卡片组件

// src/components/ArticleCard.jsx
import { Link } from 'react-router-dom';

function ArticleCard({ article }) {
  return (
    <article className="card">
      <h3>
        <Link to={`/articles/${article.id}`}>{article.title}</Link>
      </h3>
      <p>{article.summary}</p>
      <small>{article.created_at} · {article.views} 次浏览</small>
    </article>
  );
}

export default ArticleCard;

路由配置

// src/App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import ArticleDetail from './pages/ArticleDetail';
import Header from './components/Header';

function App() {
  return (
    <BrowserRouter>
      <Header />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/articles/:id" element={<ArticleDetail />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;
💡 提示

前后端分离:React 负责 UI,后端(FastAPI 等)提供 API;开发时用 Vite proxy 解决跨域。

JSX 渲染文章列表

在博客项目中,用 JSX 渲染文章列表:数据处理、条件渲染、空状态。

数据处理与渲染

// src/components/ArticleList.jsx
function ArticleList({ articles, loading, error }) {
  // 加载中
  if (loading) {
    return <div className="loading">加载中...</div>;
  }

  // 错误
  if (error) {
    return <div className="error">加载失败:{error.message}</div>;
  }

  // 空状态
  if (articles.length === 0) {
    return <div className="empty">暂无文章 🎉</div>;
  }

  // 正常列表
  return (
    <div className="article-list">
      {articles.map(article => (
        <article key={article.id} className="card">
          <h3>{article.title}</h3>
          <p className="summary">{article.summary}</p>
          <div className="meta">
            <span>{article.category_name}</span>
            <span>{article.created_at?.slice(0, 10)}</span>
            <span>👁 {article.views}</span>
          </div>
          {article.tags && (
            <div className="tags">
              {article.tags.map(tag => (
                <span key={tag} className="tag">#{tag}</span>
              ))}
            </div>
          )}
        </article>
      ))}
    </div>
  );
}

export default ArticleList;

渲染要点

  • 四种状态都要处理:loading / error / empty / data
  • key 用文章 id(稳定唯一)
  • 可选链 ?.slice(0, 10) 防字段缺失报错
  • 嵌套数组(tags)用内层 map 渲染

父组件使用

function Home() {
  const { data, loading, error } = useFetch('/api/articles');
  return (
    <ArticleList
      articles={data?.items || []}
      loading={loading}
      error={error}
    />
  );
}
💡 提示

把"列表渲染"抽成独立组件,天然支持复用和测试;数据从父组件通过 Props 传入。

响应式数据

理解 React 的响应式数据:状态变化如何驱动 UI 更新。

什么是响应式

React 中,"数据(State)变化 → 组件自动重新渲染 → UI 更新"——你只管更新数据,React 负责更新界面。

function LikeButton() {
  const [liked, setLiked] = useState(false);
  const [count, setCount] = useState(0);

  const handleLike = () => {
    setLiked(!liked);
    setCount(c => c + 1);
  };

  return (
    <button className={liked ? 'liked' : ''}>
      {liked ? '❤️ 已赞' : '🤍 点赞'}({count})
    </button>
  );
}

响应式的核心机制

  1. 组件首次渲染时,读取所有 State
  2. State 变化 → React 标记该组件"需要更新"
  3. 重新调用组件函数 → 生成新 UI 描述(虚拟 DOM)
  4. diff 对比 → 只更新变化的部分(真实 DOM)

派生数据(响应式计算)

function Cart({ items }) {
  // 总价是派生数据:由 items 计算而来,不需要单独 state
  const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
  const count = items.length;

  return (
    <div>
      <p>共 {count} 件,合计 ¥{total}</p>
    </div>
  );
}

状态更新是异步的

function Example() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    console.log(count);     // 还是旧值 0(异步更新)
  };

  // 需要新值时用函数式更新
  const handleDouble = () => {
    setCount(prev => prev * 2);
  };

  return <button
}

避免多余的 state

能用派生数据(计算得出)就不要存 state;两个 state 总是同步变化时,合并成一个对象或提升到父组件。

💡 提示

"State 是唯一数据源":UI 永远由当前 State 推导,不要试图手动改 DOM。

拆分组件

拆分组件:把大组件拆成可复用的小组件,提升代码质量。

为什么要拆组件

  • 单一职责:每个组件只做一件事
  • 可复用:相同 UI 只写一次
  • 可维护:每个文件短小清晰
  • 可测试:独立单元便于测试

拆分示例:文章卡片

// 拆分前:一个大组件
function ArticleCard({ article }) {
  return (
    <div className="card">
      <div className="author">
        <img src={article.author.avatar} alt="" />
        <div>
          <p>{article.author.name}</p>
          <small>{article.created_at}</small>
        </div>
      </div>
      <h3>{article.title}</h3>
      <p>{article.summary}</p>
      <div className="actions">
        <button>收藏</button>
        <button>分享</button>
      </div>
    </div>
  );
}
// 拆分后
function Author({ author, date }) {
  return (
    <div className="author">
      <img src={author.avatar} alt={author.name} />
      <div>
        <p>{author.name}</p>
        <small>{date}</small>
      </div>
    </div>
  );
}

function CardActions({ onFavorite, onShare }) {
  return (
    <div className="actions">
      <button
      <button
    </div>
  );
}

function ArticleCard({ article }) {
  return (
    <div className="card">
      <Author author={article.author} date={article.created_at} />
      <h3>{article.title}</h3>
      <p>{article.summary}</p>
      <CardActions
        => console.log('收藏')}
        => console.log('分享')}
      />
    </div>
  );
}

拆分原则

信号行动
组件超过 100 行考虑拆分
有可复用的 UI 片段抽成独立组件
Props 过多(5+)考虑分组/拆组件
同一逻辑多处出现抽成 Hook 或组件

组合而非继承

function Panel({ title, actions, children }) {
  return (
    <div className="panel">
      <div className="panel-header">
        <h3>{title}</h3>
        {actions}
      </div>
      {children}
    </div>
  );
}

// 使用:通过 children 组合不同内容
<Panel title="文章" actions={<button>新建</button>}>
  <ArticleList />
</Panel>
💡 提示

拆分的尺度:可读性优先。不要拆得太碎(过度抽象),也不要一个大组件写到天荒地老。

路由跳转

博客项目的路由跳转:声明式导航、编程式导航、参数传递。

声明式导航(Link)

import { Link } from 'react-router-dom';

function Header() {
  return (
    <nav>
      <Link to="/">首页</Link>
      <Link to="/about">关于</Link>
      {/* 带参数 */}
      <Link to={`/articles/${article.id}`}>文章详情</Link>
      {/* 带查询参数 */}
      <Link to="/search?q=react">搜索 React</Link>
    </nav>
  );
}

编程式导航(useNavigate)

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

function LoginPage() {
  const navigate = useNavigate();

  const handleLogin = async (e) => {
    e.preventDefault();
    // 登录成功后
    navigate('/dashboard');
    // 或带 state
    navigate('/dashboard', { state: { from: 'login' } });
    // 或替换历史(防后退)
    navigate('/dashboard', { replace: true });
  };

  return <form
}

读取参数

import { useParams, useSearchParams, useLocation } from 'react-router-dom';

function ArticleDetail() {
  const { id } = useParams();                 // /articles/42 → 42
  const [searchParams] = useSearchParams();   // ?page=2
  const location = useLocation();             // 完整 location

  const page = searchParams.get('page');

  useEffect(() => {
    fetch(`/api/articles/${id}?page=${page}`).then(...);
  }, [id, page]);

  return <div>文章 {id} 第 {page} 页</div>;
}

路由守卫(登录拦截)

import { Navigate } from 'react-router-dom';

function ProtectedRoute({ children }) {
  const token = localStorage.getItem('token');

  if (!token) {
    return <Navigate to="/login" replace />;   // 未登录跳转
  }
  return children;
}

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

404 页面

function NotFound() {
  return (
    <div className="not-found">
      <h1>404</h1>
      <p>页面不存在</p>
      <Link to="/">返回首页</Link>
    </div>
  );
}

// 路由末尾加通配
<Route path="*" element={<NotFound />} />
💡 提示

SPA 路由刷新 404 问题:需要服务器把未知路径都重写到 index.html(Nginx try_files)。

副作用与数据请求

博客项目的副作用与数据请求:加载、缓存、取消、竞态处理。

基础数据请求 Hook

// src/hooks/useFetch.js
import { useState, useEffect } from 'react';

function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();   // 取消控制器
    let active = true;

    setLoading(true);
    fetch(url, { signal: controller.signal, ...options })
      .then(r => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        return r.json();
      })
      .then(d => { if (active) { setData(d); setError(null); } })
      .catch(e => {
        if (active && e.name !== 'AbortError') setError(e);
      })
      .finally(() => { if (active) setLoading(false); });

    return () => {
      active = false;           // 防止卸载后 setState
      controller.abort();       // 取消请求
    };
  }, [url]);

  return { data, loading, error };
}

export default useFetch;

处理请求竞态

// 竞态:快速切换 userId 时,旧请求可能后到
function UserPage({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let cancelled = false;

    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(data => {
        if (!cancelled) setUser(data);   // 只接受最新请求的结果
      });

    return () => { cancelled = true; };  // 切换时作废旧请求
  }, [userId]);

  return user ? <p>{user.name}</p> : null;
}

请求状态管理

// 状态枚举
const STATUS = { IDLE: 'idle', LOADING: 'loading', SUCCESS: 'success', ERROR: 'error' };

function useArticle(id) {
  const [state, setState] = useState({ status: STATUS.IDLE, data: null, error: null });

  const load = useCallback(() => {
    setState(s => ({ ...s, status: STATUS.LOADING }));
    fetch(`/api/articles/${id}`)
      .then(r => r.json())
      .then(data => setState({ status: STATUS.SUCCESS, data, error: null }))
      .catch(error => setState({ status: STATUS.ERROR, data: null, error }));
  }, [id]);

  useEffect(load, [load]);   // 加载

  return { ...state, reload: load };   // 支持手动刷新
}

防抖搜索

function SearchBox({ onSearch }) {
  const [keyword, setKeyword] = useState('');

  useEffect(() => {
    const timer = setTimeout(() => onSearch(keyword), 300);
    return () => clearTimeout(timer);   // 输入中断时清除
  }, [keyword]);

  return (
    <input
      value={keyword}
      => setKeyword(e.target.value)}
      placeholder="搜索文章..."
    />
  );
}
💡 提示

请求副作用三件套:loading 状态、错误处理、取消/防竞态。复杂应用可上 TanStack Query 自动处理这些。

useEffect 进阶

useEffect 进阶:依赖管理、防重复执行、与 ref 配合、严格模式。

依赖数组的深水区

function Example({ user }) {
  // ❌ 对象依赖:每次渲染都是新引用 → 死循环风险
  useEffect(() => {
    // ...
  }, [user]);   // user 对象每次都是新引用

  // ✅ 拆出原始值作为依赖
  useEffect(() => {
    // ...
  }, [user.id]);

  // ✅ 用 useMemo 稳定引用
  const stableUser = useMemo(() => user, [user.id]);
  useEffect(() => {
    // ...
  }, [stableUser]);
}

只在更新时执行(跳过首次)

function Counter() {
  const [count, setCount] = useState(0);
  const firstRender = useRef(true);

  useEffect(() => {
    if (firstRender.current) {
      firstRender.current = false;   // 跳过首次
      return;
    }
    console.log('count 变化了:', count);   // 只在更新时打印
  }, [count]);

  return <button => setCount(c => c + 1)}>{count}</button>;
}

依赖函数时用 useCallback

function Parent() {
  const [id, setId] = useState(1);

  // 函数依赖:用 useCallback 保证引用稳定
  const loadData = useCallback(() => {
    fetch(`/api/data/${id}`).then(...);
  }, [id]);   // id 变化才重建函数

  useEffect(() => {
    loadData();
  }, [loadData]);   // 依赖稳定,不会死循环

  return <Child />;
}

useLayoutEffect(测量布局)

import { useLayoutEffect, useRef, useState } from 'react';

function Measure() {
  const ref = useRef(null);
  const [width, setWidth] = useState(0);

  // 同步执行(在浏览器绘制前),避免闪烁
  useLayoutEffect(() => {
    setWidth(ref.current.offsetWidth);
  }, []);

  return <div ref={ref}>宽度:{width}px</div>;
}

React 18 StrictMode 双执行

开发模式下 effect 会执行两次(mount → unmount → mount),用于暴露清理缺失问题。生产环境只执行一次,无需担心。

💡 提示

useEffect 的黄金法则:依赖数组写全(用 eslint-plugin-react-hooks 自动检查),清理函数记得返回。

自定义 Hook

自定义 Hook:抽取可复用逻辑,让组件更简洁。这是 React 逻辑复用的核心手段。

什么是自定义 Hook

以 use 开头的函数,内部可以使用其他 Hook,把可复用的逻辑(数据请求、表单、主题等)封装起来。

useToggle(布尔切换)

// useToggle.js
import { useState, useCallback } from 'react';

function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue(v => !v), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);

  return { value, toggle, setTrue, setFalse };
}

// 使用
function Modal() {
  const { value: isOpen, setTrue: open, setFalse: close } = useToggle();

  return (
    <>
      <button
      {isOpen && <div className="modal"
    </>
  );
}

useLocalStorage(本地存储)

// useLocalStorage.js
import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  // 惰性初始化:读取已有值
  const [value, setValue] = useState(() => {
    const saved = localStorage.getItem(key);
    return saved ? JSON.parse(saved) : initialValue;
  });

  // 自动同步到 localStorage
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

// 使用:主题持久化
function App() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  return (
    <div className={theme}>
      <button => setTheme(theme === 'light' ? 'dark' : 'light')}>
        切换主题
      </button>
    </div>
  );
}

useDebounce(防抖值)

// useDebounce.js
import { useState, useEffect } from 'react';

function useDebounce(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debounced;
}

// 使用:搜索防抖
function Search() {
  const [keyword, setKeyword] = useState('');
  const debouncedKeyword = useDebounce(keyword, 500);

  useEffect(() => {
    if (debouncedKeyword) {
      // 500ms 后执行搜索
      searchAPI(debouncedKeyword);
    }
  }, [debouncedKeyword]);

  return <input value={keyword} => setKeyword(e.target.value)} />;
}

自定义 Hook 的最佳实践

  • 命名以 use 开头(React 约定)
  • 返回"状态 + 操作"的组合
  • 内部使用多个 Hook 没问题
  • 放 src/hooks/ 目录统一管理
💡 提示

先写普通代码,发现多处重复时再抽成 Hook——重构优于提前设计。

全局状态

博客项目的全局状态管理:Context、Zustand、Redux 的选型与实践。

什么时候需要全局状态

  • 多个组件共享同一份数据(用户信息、主题)
  • 组件层级很深,Props 逐层传递很痛苦
  • 跨页面共享状态(登录态、购物车)

方案一:Context(内置,够用)

// src/store/AuthContext.jsx
import { createContext, useContext, useState } from 'react';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [token, setToken] = useState(localStorage.getItem('token'));

  const login = (data) => {
    setToken(data.token);
    setUser(data.user);
    localStorage.setItem('token', data.token);
  };

  const logout = () => {
    setToken(null);
    setUser(null);
    localStorage.removeItem('token');
  };

  return (
    <AuthContext.Provider value={{ user, token, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => useContext(AuthContext);
// 使用
import { useAuth } from '../store/AuthContext';

function Header() {
  const { user, logout } = useAuth();

  return user ? (
    <>
      <span>你好,{user.name}</span>
      <button
    </>
  ) : (
    <Link to="/login">登录</Link>
  );
}

方案二:Zustand(轻量推荐)

npm install zustand
// src/store/useStore.js
import { create } from 'zustand';

const useStore = create((set) => ({
  favorites: [],
  addFavorite: (id) => set(s => ({ favorites: [...s.favorites, id] })),
  removeFavorite: (id) => set(s => ({ favorites: s.favorites.filter(f => f !== id) })),
}));

// 任何组件中直接使用
function FavoriteButton({ articleId }) {
  const favorites = useStore(s => s.favorites);
  const addFavorite = useStore(s => s.addFavorite);
  const isFav = favorites.includes(articleId);

  return (
    <button => (isFav ? removeFavorite(articleId) : addFavorite(articleId))}>
      {isFav ? '❤️' : '🤍'}
    </button>
  );
}

方案对比

方案复杂度性能适用
Context低中低频全局数据(用户/主题)
Zustand低高中小项目(推荐)
Redux高高大型复杂应用
💡 提示

多数博客项目用 Context + useReducer 就够;状态复杂、更新频繁再引入 Zustand。

打包上线

React 项目打包上线:构建、部署、配置优化。

构建生产版本

npm run build
# 输出到 dist/ 目录(压缩、哈希文件名)

部署到 Nginx(静态托管)

server {
    listen 443 ssl http2;
    server_name blog.laowang.click;

    root /www/wwwroot/blog/dist;    # 构建产物目录
    index index.html;

    # SPA 路由:所有路径都回退到 index.html
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 静态资源缓存
    location /assets/ {
        expires 30d;
        add_header Cache-Control "public";
    }

    # API 反代到后端
    location /api/ {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

SPA 刷新 404 的解决

关键配置:try_files $uri $uri/ /index.html;——所有未匹配的路径都返回 index.html,由前端路由接管。

环境变量

// .env.development
VITE_API_BASE=/api

// .env.production
VITE_API_BASE=https://api.laowang.click

// 代码中使用
const API = import.meta.env.VITE_API_BASE;

构建优化

  • 路由懒加载(lazy + Suspense)
  • 压缩图片、代码分包(Vite 自动)
  • gzip/brotli 压缩(Nginx 开启)
gzip on;
gzip_types text/css application/javascript application/json;
gzip_min_length 1k;

部署到宝塔面板

  1. 本地 npm run build 生成 dist
  2. 宝塔新建纯静态站点,把 dist 内容上传到站点目录
  3. Nginx 配置 try_files 支持 SPA 路由
  4. 申请 SSL 证书,开启 HTTPS
  5. 配置 API 反代(/api → 后端 8000)

上线后检查

  • 访问域名:页面正常、刷新不 404
  • 控制台无报错
  • API 请求正常(无跨域错误)
💡 提示

SPA 部署三件套:dist 上传、try_files 回退、API 反代。记住这三点,部署基本不踩坑。

第 1 / 42 节