Archived
vault backup: 2026-04-29 22:19:51
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
---
|
||||
tags: [React, TypeScript, Frontend, Vite]
|
||||
create time: 2026-04-29 22:00
|
||||
---
|
||||
|
||||
# 环境搭建与项目结构
|
||||
|
||||
## 概述
|
||||
|
||||
本文档作为 React 开发系列的起点,介绍从零搭建 **React + TypeScript** 开发环境的全过程。内容包括:主流脚手架工具的选型对比、Vite 项目初始化与核心配置、推荐的目录结构与文件命名规范。完成本章后,你将拥有一个开箱即用的工程化项目骨架,为后续学习组件开发、状态管理和路由打下基础。
|
||||
|
||||
> [!tip] 前置知识
|
||||
> - 已安装 **Node.js 18+**(推荐 LTS 版本)
|
||||
> - 熟悉基本的终端操作和 npm 命令
|
||||
> - 了解 JavaScript/TypeScript 基本语法
|
||||
|
||||
## 选型决策:Vite vs CRA vs Next.js
|
||||
|
||||
> [!question] 思考:为什么不再推荐使用 Create React App?
|
||||
|
||||
Create React App 已经停止维护,其 Webpack 构建在大型项目中存在明显的冷启动慢、HMR 速度慢等问题。以下是三种方案的横向对比:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[项目类型] --> B["纯 SPA"]
|
||||
A --> C["SSR / 全栈应用"]
|
||||
B --> D[Vite + React]
|
||||
C --> E[Next.js App Router]
|
||||
D --> F["适合场景:前端主导、后端提供 API"]
|
||||
E --> G["适合场景:SEO 优先、服务端渲染需求"]
|
||||
|
||||
style D fill:#61DAFB,color:#000
|
||||
style E fill:#000
|
||||
```
|
||||
|
||||
### Vite 方案(本文档主推)
|
||||
|
||||
Vite 基于原生 ES Modules + esbuild,实现毫秒级热更新。相比传统的 Webpack 方案,开发服务器启动时间从数十秒降至亚秒级。
|
||||
|
||||
```bash
|
||||
# 创建项目(选择 react-ts 模板)
|
||||
npm create vite@latest my-app -- --template react-ts
|
||||
cd my-app
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器(自动打开浏览器)
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Vite 核心配置
|
||||
|
||||
`vite.config.ts` 是开发服务器的中枢,通常需要配置路径别名、代理和插件:
|
||||
|
||||
```ts
|
||||
// vite.config.ts
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()], // SWC 编译器比 Babel 更快
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'), // @ 别名指向 src 目录
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': 'http://localhost:8080', // 开发时代理后端请求
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
> [!note] 关键配置说明
|
||||
> - **SWC vs Babel**:`@vitejs/plugin-react-swc` 使用 Rust 编写的 SWC 编译器,编译速度显著优于传统 Babel 方案
|
||||
> - **路径别名**:配合 `tsconfig.json` 中的 `paths` 配置,可以消除项目中深层的相对导入(`../../../utils` → `@/utils`)
|
||||
> - **代理**:本地开发时将 `/api` 开头的请求转发到后端服务,避免跨域问题
|
||||
|
||||
### Next.js 方案
|
||||
|
||||
```bash
|
||||
# --app: 使用 App Router(推荐)
|
||||
# --typescript: 启用 TypeScript
|
||||
# --tailwind: 集成 Tailwind CSS
|
||||
# --src-dir: 代码放在 src 目录
|
||||
npx create-next-app@latest my-app --typescript --app --tailwind --src-dir
|
||||
```
|
||||
|
||||
> [!warning] 选型建议
|
||||
> - 如果你的项目以 **SEO、首屏加载速度、全栈能力** 为核心需求,选 Next.js
|
||||
> - 如果团队更擅长 **纯前端开发**,后端由独立 API 服务提供,Vite + React 是更轻量合理的选择
|
||||
|
||||
## 目录结构
|
||||
|
||||
### 标准组织方式
|
||||
|
||||
推荐的 React + TypeScript 项目目录结构如下:
|
||||
|
||||
```
|
||||
src/
|
||||
├── assets/ # 静态资源(图片、字体等)
|
||||
├── components/ # 通用 UI 组件(无状态或低状态)
|
||||
│ ├── Button/
|
||||
│ │ ├── Button.tsx
|
||||
│ │ ├── Button.test.tsx
|
||||
│ │ └── index.ts
|
||||
│ └── Modal/
|
||||
├── hooks/ # 自定义 Hooks
|
||||
├── pages/ # 页面级组件
|
||||
├── routes/ # 路由配置
|
||||
├── stores/ # 状态管理(Zustand / Redux)
|
||||
├── types/ # TypeScript 类型定义
|
||||
├── utils/ # 工具函数
|
||||
├── App.tsx # 根组件
|
||||
└── main.tsx # 入口文件
|
||||
```
|
||||
|
||||
### 模块依赖关系
|
||||
|
||||
理解各目录之间的引用关系对维护项目至关重要:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
MAIN[main.tsx] --> APP[App.tsx]
|
||||
APP --> PAGES[pages - 页面组件]
|
||||
APP --> ROUTES[routes - 路由配置]
|
||||
PAGES --> COMPONENTS[components - UI组件]
|
||||
PAGES --> HOOKS[hooks - 自定义Hooks]
|
||||
PAGES --> STORES[stores - 状态管理]
|
||||
COMPONENTS --> HOOKS
|
||||
COMPONENTS --> UTILS[utils - 工具函数]
|
||||
COMPONENTS --> TYPES[types - 类型定义]
|
||||
STORES --> TYPES
|
||||
ROUTES --> PAGES
|
||||
```
|
||||
|
||||
> [!note] 依赖方向原则
|
||||
> - **单向依赖**:上层模块(pages/stores)依赖下层模块(components/hooks/utils),反向依赖会导致循环引用
|
||||
> - **types 是基础设施**:被所有层共享,但绝不反过来依赖其他业务模块
|
||||
|
||||
> [!tip] 目录组织原则
|
||||
> - **按功能而非按类型**:大型项目推荐使用 Feature-Sliced Design 或 Atomics 模式,将组件、Hook、样式、测试放在同一目录下
|
||||
> - **Barrel Export(统一导出)**:每个子目录通过 `index.ts` 导出统一接口,简化上游 import 路径
|
||||
|
||||
## 关键配置文件
|
||||
|
||||
### tsconfig.json 核心选项
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020", // 目标 JS 版本
|
||||
"module": "ESNext", // 模块系统
|
||||
"jsx": "react-jsx", // React 17+ 自动导入 JSX transform
|
||||
"strict": true, // 开启严格模式
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"] // 路径别名
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ESLint + Prettier 组合
|
||||
|
||||
推荐使用 **ESLint flat config**(`eslint.config.js`),这是 ESLint 9 引入的新格式:
|
||||
|
||||
```js
|
||||
// eslint.config.js (flat config)
|
||||
import js from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import globals from 'globals'
|
||||
|
||||
export default tseslint.config(
|
||||
js.configs.recommended,
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
globals: { ...globals.browser },
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
},
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
> [!tip] ESLint 迁移提示
|
||||
> - 旧版 `.eslintrc.*` 格式的配置文件可以通过 `eslint --init` 自动迁移为 flat config
|
||||
> - `@typescript-eslint` 同时接管了 TypeScript 特有规则和 JavaScript 规则,无需再单独安装 `eslint-plugin-typescript`
|
||||
> - React 19 的 ESLint 插件正在逐步推出新特性,建议保持版本同步更新
|
||||
|
||||
## 开发习惯建议
|
||||
|
||||
| 工具 | 用途 | 推荐版本 |
|
||||
|------|------|----------|
|
||||
| Vite | 构建 & HMR | ^6.x |
|
||||
| TypeScript | 类型安全 | ^5.x |
|
||||
| ESLint | 代码质量 | ^9.x (flat config) |
|
||||
| Prettier | 格式化 | ^3.x |
|
||||
| Vitest | 单元测试 | ^3.x |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/REACT/README]]
|
||||
- [[hhs/REACT/1. 基础篇/02-JSX 语法]]
|
||||
- [[hhs/REACT/3. 生态工具篇/08-路由管理]]
|
||||
- [[hhs/REACT/5. 工程实践篇/16-测试]]
|
||||
@@ -0,0 +1,296 @@
|
||||
---
|
||||
tags: [React, JSX, TSX, Frontend]
|
||||
create time: 2026-04-29 22:01
|
||||
---
|
||||
|
||||
# JSX 语法
|
||||
|
||||
## 概述
|
||||
|
||||
JSX(JavaScript XML)是 React 的核心语法糖。它让开发者能够在 JavaScript 中直接编写类似 HTML 的结构,经编译后转化为 `React.createElement()` 调用。理解 JSX 的底层机制是掌握 React 渲染原理的基础。
|
||||
|
||||
## JSX 的本质
|
||||
|
||||
JSX 并不是模板字符串,编译后会变成普通的 JavaScript 对象:
|
||||
|
||||
```jsx
|
||||
// 你写的 JSX
|
||||
const element = <h1 className="title">Hello, World!</h1>;
|
||||
|
||||
// 编译后的产物
|
||||
const element = React.createElement(
|
||||
"h1",
|
||||
{ className: "title" },
|
||||
"Hello, World!"
|
||||
);
|
||||
```
|
||||
|
||||
> [!warning] 关键点
|
||||
> - `React.createElement` 返回的是一个 **Plain Object**(虚拟 DOM 节点),不是真实的 DOM 元素
|
||||
> - 这也是为什么 JSX 不会直接操作浏览器 API,而是由 React 的 Reconciler 统一管理
|
||||
|
||||
## 表达式嵌入
|
||||
|
||||
在 JSX 中用 `{}` 包裹 JavaScript 表达式:
|
||||
|
||||
```jsx
|
||||
function FormattedDate({ date }) {
|
||||
return (
|
||||
<>
|
||||
<h1>Hello!</h1>
|
||||
<h2>今天 {date.toLocaleDateString()}.</h2>
|
||||
{/* 条件:JSX 不能用 if-else,用三元运算符或逻辑与 */}
|
||||
{date.getTime() < Date.now() && <p>日期已过期</p>}
|
||||
{/* 数组渲染:每个元素需要 key prop */}
|
||||
{[1, 2, 3].map(n => (
|
||||
<span key={n}>{n}</span>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 允许与不允许的表达式
|
||||
|
||||
| ✅ 允许 | ❌ 不允许 |
|
||||
|---------|----------|
|
||||
| `{variable}` | `{if (x) ...}` — JSX 中不支持语句 |
|
||||
| `{arr.map(...)}` | `{for (let i...)}` — 同上 |
|
||||
| `{cond && <Comp />}` | `{func()}` — 函数无返回值时渲染 `undefined`(不报错但不生效)|
|
||||
| `{null}`, `{false}` | `{""}`, `{0}` — 会渲染空字符串或数字 `0`,常见陷阱 |
|
||||
|
||||
> [!question] 为什么 `{cond ? <A /> : func()}` 能跑?
|
||||
> - 这是合法的 JS 表达式——只要 `func()` 返回一个值即可
|
||||
> - 但建议写成 `{cond ? <A /> : <B />}`,保持视觉一致性,避免混用 JSX 和函数调用
|
||||
|
||||
> [!note] null / undefined / boolean vs "" / 0
|
||||
> - `null`、`undefined`、`true`、`false` 渲染结果为空——这是有意设计
|
||||
> - `0` 和 `""` 会输出到 DOM,常见陷阱
|
||||
|
||||
## 条件渲染模式
|
||||
|
||||
```jsx
|
||||
// 模式1:&& 短路(推荐用于简单场景)
|
||||
{isLoggedIn && <AdminDashboard />}
|
||||
|
||||
// 模式2:三元运算符(适合两种视图切换)
|
||||
{isLoading ? <Spinner /> : <Content />}
|
||||
|
||||
// 模式3:提取为组件(复杂条件逻辑)
|
||||
return <UserCard user={user} showActions={isAdmin} />;
|
||||
|
||||
// 模式4:IIFE(极少使用,仅临时调试)
|
||||
{(function () {
|
||||
if (!data) return <EmptyState />;
|
||||
return <DataDisplay data={data} />;
|
||||
})()}
|
||||
```
|
||||
|
||||
## 列表渲染与 Key
|
||||
|
||||
```tsx
|
||||
interface TodoItem {
|
||||
id: number;
|
||||
text: string;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
function TodoList({ items }: { items: TodoItem[] }) {
|
||||
return (
|
||||
<ul>
|
||||
{items.map((item) => (
|
||||
<li key={item.id}>
|
||||
<input type="checkbox" checked={item.completed} readOnly />
|
||||
{item.text}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> [!question] 思考:为什么 React 要求列表项必须有 key?
|
||||
> - Key 帮助 React 识别哪些条目变了/增了/删了,从而最小化重渲染次数
|
||||
> - 不建议用 index 作为 key(当列表顺序变化时会导致状态错乱)
|
||||
|
||||
## JSX 事件绑定
|
||||
|
||||
```jsx
|
||||
function ClickCounter() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
// 驼峰命名,不要写 onClick="handler()"
|
||||
const handleClick = () => setCount(c => c + 1);
|
||||
|
||||
// 阻止默认行为
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={handleClick}>Count: {count}</button>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input type="text" name="q" />
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 事件传参
|
||||
|
||||
当需要向事件处理函数传递参数时,有两种写法:
|
||||
|
||||
```tsx
|
||||
interface TodoItemProps {
|
||||
id: number;
|
||||
onDelete: (id: number) => void;
|
||||
}
|
||||
|
||||
function TodoItem({ id, onDelete }: TodoItemProps) {
|
||||
return (
|
||||
<button onClick={() => onDelete(id)}>
|
||||
{/* ⚡ 箭头函数包裹:创建闭包捕获当前 id */}
|
||||
删除
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> [!warning] 常见陷阱
|
||||
> - ~~`onClick={onDelete(id)}`~~ ❌ — 会在渲染时立即执行 `onDelete`,而非点击时执行
|
||||
> - ~~`onClick={onDelete}`~~ — 缺少参数,不知道要删除哪个
|
||||
> - ✅ **正确姿势**:`() => onDelete(id)` — 用箭头函数延迟执行并捕获参数
|
||||
> - 💡 如果不需要参数,直接传引用即可:`onClick={handleClick}`(不要加括号)
|
||||
|
||||
### 常用合成事件类型
|
||||
|
||||
```ts
|
||||
type MouseEvents = React.MouseEvent<HTMLButtonElement>;
|
||||
type FormEvents = React.FormEvent<HTMLFormElement>;
|
||||
type ChangeEvents = React.ChangeEvent<HTMLInputElement>;
|
||||
type KeyboardEvents = React.KeyboardEvent<HTMLDivElement>;
|
||||
```
|
||||
|
||||
> [!tip] TypeScript 事件类型的推导技巧
|
||||
> - 不传泛型时:`event.target` 类型为 `EventTarget`(信息最少)
|
||||
> - 传入具体元素后:`event.currentTarget` 可以精确定位到对应 DOM 类型
|
||||
> - 💡 **最佳实践**:优先使用 `currentTarget` 而非 `target`,避免类型收窄问题
|
||||
|
||||
## JSX 基本规则
|
||||
|
||||
写 JSX 时有几条硬性规则需要遵守:
|
||||
|
||||
```tsx
|
||||
// 规则1:自闭合标签 —— 无子元素的元素必须加 /
|
||||
<img src={avatar} alt="头像" />
|
||||
<Input placeholder="搜索..." />
|
||||
|
||||
// 规则2:className 替代 class
|
||||
<div className="card">内容</div>
|
||||
// <div class="card"> ❌ — "class"是JS保留字
|
||||
|
||||
// 规则3:style 接收对象而非字符串
|
||||
<div style={{ color: "red", fontSize: 16 }}>样式</div>
|
||||
// 驼峰命名:fontSize, backgroundColor, gridColumn 等
|
||||
|
||||
// 规则4:单根节点或 Fragment
|
||||
const Card = ({ title, children }) => (
|
||||
<> {/* ✅ Fragment:不产生额外 DOM 节点 */}
|
||||
<h2>{title}</h2>
|
||||
<div>{children}</div>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
> [!note] key 属性的唯一要求
|
||||
> - `key` 只在数组上下文中有效——它是 React 的元数据,不会被传递给组件
|
||||
> - 在自定义组件上使用 key 会报错:**key 只能在列表子元素上使用**
|
||||
|
||||
## Props 传递方式
|
||||
|
||||
```tsx
|
||||
interface CardProps {
|
||||
title: string;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function Card({ title, className = "", children }: CardProps) {
|
||||
return (
|
||||
<div className={`card ${className}`}>
|
||||
<h3>{title}</h3>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 展开 props
|
||||
const attrs = { className: "card", title: "详情" };
|
||||
<Card {...attrs} />;
|
||||
|
||||
// 动态属性名
|
||||
<div style={{ color: isActive ? "red" : "black" }}>
|
||||
|
||||
// 受控 props(父组件控制子组件内部 state)
|
||||
<Select value={selectedValue} onChange={setSelectedValue} />
|
||||
|
||||
// 函数作为 prop(常见于表单回调)
|
||||
<SearchBar onSearch={(q) => setQuery(q)} />
|
||||
```
|
||||
|
||||
> [!question] 为什么 children 的类型是 React.ReactNode 而不是 JSX.Element?
|
||||
> - `JSX.Element` 只代表 React 元素(如 `<div />`、`<Comp />`)
|
||||
> - `React.ReactNode` 包含更多可能:字符串、数字、Fragment、甚至 null/undefined
|
||||
> - 💡 使用 `JSX.Element` 会报错当子元素是纯文本或数组时
|
||||
|
||||
## JSX 与模板引擎对比
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[JSX] --> B["编译期检查"]
|
||||
A --> C["完整的 JavaScript 能力"]
|
||||
A --> D["类型安全(TS)"]
|
||||
|
||||
E["模板引擎 v-html/v-if"] --> F["运行时解析"]
|
||||
E --> G["字符串插值 {{}}"]
|
||||
E --> H["有限的 JS 表达式"]
|
||||
|
||||
style A fill:#61DAFB,color:#000
|
||||
style E fill:#42B883,color:#fff
|
||||
```
|
||||
|
||||
> [!tip] JSX 的核心优势
|
||||
> 1. **强类型** — 配合 TypeScript 编译期拦截错误
|
||||
> 2. **零额外语法** — 没有 {{}} / v-if / v-for 等新规则,一切在 JS 中
|
||||
> 3. **JS 全能力** — 解构、spread、闭包、高阶函数随时可用
|
||||
|
||||
## JSX 渲染流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as 开发者
|
||||
participant Babel as Babel/TSX
|
||||
participant React as React Runtime
|
||||
participant VDOM as Fiber (虚拟 DOM)
|
||||
participant Diff as Reconciler Diff
|
||||
participant BM as Browser API
|
||||
|
||||
Dev->>Babel: 编写 JSX:<h1>Hello</h1>
|
||||
Note over Dev,Babel: "编译期转换,非运行时"
|
||||
Babel->>Babel: 转译成 createElement()
|
||||
Babel->>React: React.createElement("h1", null, "Hello")
|
||||
Note over React: 返回 VDOM Plain Object
|
||||
React->>VDOM: 创建/更新 Fiber 树
|
||||
VDOM->>Diff: Diff 算法比较新旧树
|
||||
Diff->>Diff: 找出最小变更集
|
||||
Diff->>BM: patch 真实 DOM
|
||||
Note over BM: "批量更新,一次 Reflow/Repaint"
|
||||
```
|
||||
|
||||
> [!note] 一句话总结渲染流程
|
||||
> JSX → `createElement()` → VDOM → **Diff** → patch 真实 DOM
|
||||
>
|
||||
> JSX 的每次重新渲染都经历完整的 Diff 过程——这也是为什么理解 key 的作用至关重要。
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
---
|
||||
tags: [React, Components, Props, Frontend]
|
||||
create time: 2026-04-29 22:02
|
||||
---
|
||||
|
||||
# 组件与 Props
|
||||
|
||||
## 概述
|
||||
|
||||
React 应用由一组可复用的组件构成。理解组件的拆分粒度、Props 的类型安全传递,以及组合优于继承的设计哲学,是写出高质量 React 代码的核心。本章从组件的基本形态讲起,深入到 Props 的校验与传递模式,最后介绍组件组合的高级技巧。
|
||||
|
||||
> [!tip] 本节学习路线
|
||||
> 1. 函数组件 vs 类组件 → 为什么函数组件成为主流
|
||||
> 2. 组件拆分原则 → 如何找到合理的职责边界
|
||||
> 3. Props 定义与校验 → TypeScript 方式(推荐)和 PropTypes 方式
|
||||
> 4. Props 传递模式 → 基础透传、回调通信、children 插槽
|
||||
> 5. Composition 高级模式 → React.memo、forwardRef、受控组件
|
||||
> 6. Context 初探 → 轻量级跨层级通信
|
||||
|
||||
## 函数组件 vs 类组件
|
||||
|
||||
```tsx
|
||||
// ✅ 函数组件(当前推荐写法)
|
||||
// 输入 props,输出 JSX —— 纯函数的直观映射
|
||||
function Greeting({ name }: { name: string }) {
|
||||
return <h1>Hello, {name}</h1>;
|
||||
}
|
||||
|
||||
// ⚠️ 类组件(遗留项目常见,新项目中应避免)
|
||||
// this.props + render() —— 需要额外处理 this 绑定
|
||||
class Greeting extends React.Component<{ name: string }> {
|
||||
render() {
|
||||
return <h1>Hello, {this.props.name}</h1>;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 为什么推荐函数组件?
|
||||
|
||||
| 对比维度 | 函数组件 + Hooks | 类组件 |
|
||||
|----------|------------------|--------|
|
||||
| 心智模型 | 纯函数:输入 props → 输出 JSX | 实例方法 + this 绑定 |
|
||||
| 逻辑复用 | Custom Hooks(声明式组合) | HOC / Render Props(嵌套地狱)|
|
||||
| 类型推导 | TS 自动推断完善 | 需要手动泛型参数 |
|
||||
| 性能开销 | 无实例创建成本 | new 实例 + bind overhead |
|
||||
| Suspense | 完全支持 | 不支持 |
|
||||
| Hooks 绑定 | 基于调用顺序 | 依赖 this 上下文 |
|
||||
|
||||
> [!note] Hooks 绑定的本质
|
||||
> useState、useEffect 等 Hook 按调用顺序内部维护一个链表。这就是为什么 Hook **不能写在条件语句或循环中**——顺序一旦改变,状态就会错配。
|
||||
|
||||
## 组件拆分原则
|
||||
|
||||
> [!question] 思考:一个按钮算一个独立组件,还是把按钮和其外层容器一起写?
|
||||
>
|
||||
> 如果按钮只在这一处使用,合并更简洁;如果多个地方共用同一个按钮样式或交互逻辑,就应该拆出来。核心判断标准:**这个组件是否有独立的业务语义?**
|
||||
|
||||
好的拆分遵循 **"单一职责" + "合理抽象"**。下面用流程图展示不同层级的职责划分:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph P ["Page Level — 页面组装层"]
|
||||
A["AboutPage<br/>拼接各功能模块"]
|
||||
end
|
||||
|
||||
subgraph F ["Feature Level — 功能模块层"]
|
||||
B["UserProfile<br/>头像 + 昵称 + 设置入口"]
|
||||
C["Dashboard<br/>图表 + 数据表格"]
|
||||
end
|
||||
|
||||
subgraph C2 ["Component Level — 通用 UI 层"]
|
||||
D["Button<br/>按钮变体 & 尺寸"]
|
||||
E["Modal<br/>弹窗外壳"]
|
||||
end
|
||||
|
||||
subgraph PL ["Primitive Level — 原子层"]
|
||||
F2["IconButton<br/>图标按钮"]
|
||||
G["Avatar<br/>头像图片"]
|
||||
end
|
||||
|
||||
A --> B
|
||||
A --> C
|
||||
B --> D
|
||||
B --> G
|
||||
C --> E
|
||||
D --> F2
|
||||
```
|
||||
|
||||
### 判断是否该拆分的标准
|
||||
|
||||
1. **可复用性** — 同一组 JSX 出现两次以上,考虑抽取为独立组件
|
||||
2. **可读性** — 单个文件超过 300 行、单个组件超过 80 行,应考虑拆分
|
||||
3. **测试粒度** — 难以单独测试的巨型组件应拆为小组件
|
||||
4. **业务语义** — 每个组件应对应一个清晰的业务概念
|
||||
|
||||
```tsx
|
||||
// ❌ 反例:巨型组件,混合了多个不相关的职责
|
||||
function Dashboard() {
|
||||
// 200+ 行,混合了 Header、Sidebar、DataTables、Chart...
|
||||
// 改 Header 要遍历整文件,测试也要 mock 所有子模块
|
||||
}
|
||||
|
||||
// ✅ 正例:拆分为功能块,每个组件职责单一
|
||||
function Dashboard() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader /> {/* 专注头部信息 */}
|
||||
<aside><Sidebar /></aside> {/* 专注侧边导航 */}
|
||||
<main>
|
||||
<DataTable data={tasks} /> {/* 专注数据表格 */}
|
||||
<Chart data={stats} /> {/* 专注图表展示 */}
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Props 定义与类型校验
|
||||
|
||||
### TypeScript 接口方式(推荐)
|
||||
|
||||
```tsx
|
||||
interface ButtonProps {
|
||||
label: string; // 必填:按钮文字
|
||||
variant?: "primary" | "secondary" | "danger"; // 可选:视觉变体(联合类型约束)
|
||||
size?: "sm" | "md" | "lg"; // 可选:尺寸档位
|
||||
disabled?: boolean; // 可选:禁用态
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void; // 可选:点击回调
|
||||
children?: React.ReactNode; // 可选:内容插槽
|
||||
}
|
||||
|
||||
// 方式1:解构赋值设默认值(适合简单默认值)
|
||||
function Button({ label, variant = "primary", onClick }: ButtonProps) {
|
||||
return <button className={`btn btn-${variant}`} onClick={onClick}>{label}</button>;
|
||||
}
|
||||
|
||||
// 方式2:全部字段显式声明默认值(适合需要覆盖全部可选字段时)
|
||||
function Button({ label, variant = "primary", size = "md", disabled = false, children }: ButtonProps) {
|
||||
return <button className={`btn btn-${variant} btn-${size}`} disabled={disabled}>{children}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
> [!tip] 类型定义的位置建议
|
||||
> - 小型组件:interface 与组件放在同一文件中
|
||||
> - 大型组件库:type 导出到独立的 `types.ts`,便于跨文件引用和复用
|
||||
|
||||
### PropTypes 方式(JS 项目或过渡期)
|
||||
|
||||
```js
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
Button.propTypes = {
|
||||
label: PropTypes.string.isRequired, // 必填字符串
|
||||
variant: PropTypes.oneOf(["primary", "secondary"]), // 枚举限制
|
||||
onClick: PropTypes.func, // 可选函数
|
||||
};
|
||||
```
|
||||
|
||||
> [!warning] 不要混用两种校验方式
|
||||
> TS 提供**编译时检查**,PropTypes 是**运行时兜底**。选其一即可,新项目统一用 TS。混用会导致维护和调试成本翻倍。
|
||||
|
||||
## Props 传递模式
|
||||
|
||||
### 基础传递
|
||||
|
||||
```tsx
|
||||
// 1. 直接传递字面量值
|
||||
<Button label="提交" disabled />
|
||||
|
||||
// 2. 传递变量或表达式
|
||||
<Button label={submitText} onClick={handleSubmit} />
|
||||
```
|
||||
|
||||
### 展开透传(Spread Props)
|
||||
|
||||
```tsx
|
||||
// 当一层组件需要将自身 props 原样传给子组件时,避免逐个手写
|
||||
const inputProps = { placeholder: "请输入", onChange: handleChange };
|
||||
<Input {...inputProps} /> // ⚡ Spread Operator 将对象解包为独立的 props
|
||||
|
||||
// 局部覆盖:父 prop 优先级低于子 prop(后者覆盖前者)
|
||||
<Input {...inputProps} autoFocus /> // autoFocus 额外传入,其余来自 inputProps
|
||||
```
|
||||
|
||||
### 回调 Prop(子 → 父通信)
|
||||
|
||||
```tsx
|
||||
// 子组件通过回调将数据或事件通知父组件
|
||||
<Form onSubmit={(data) => console.log("收到表单数据:", data)} />
|
||||
// 💡 父组件将处理函数作为 prop 传入,子组件在合适时机调用它
|
||||
```
|
||||
|
||||
### Render Item Prop(列表渲染定制)
|
||||
|
||||
```tsx
|
||||
// 父组件告诉子组件:"每个 item 长什么样"
|
||||
<List
|
||||
items={items}
|
||||
renderItem={(item) => <ListItem key={item.id} {...item} />}
|
||||
/>
|
||||
// 📌 比完整的 Render Props 模式更轻量的写法
|
||||
```
|
||||
|
||||
## Children 与 Composition
|
||||
|
||||
```tsx
|
||||
// Card 接收 children(任意 JSX 内容),灵活度远超固定 string prop
|
||||
function Card({ children, header }: { children: React.ReactNode; header: string }) {
|
||||
return (
|
||||
<div className="card">
|
||||
<h3>{header}</h3>
|
||||
<div className="card-body">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 使用示例:children 可以包含任意合法的 JSX
|
||||
<Card header="用户信息">
|
||||
<p>姓名:张三</p>
|
||||
<p>年龄:25</p>
|
||||
</Card>
|
||||
|
||||
// Fragment —— 不产生额外 DOM 节点,用于包裹兄弟元素
|
||||
function ListItem({ text, icon }: { text: string; icon: React.ReactNode }) {
|
||||
return (
|
||||
<li>
|
||||
<> {/* ⚡ Fragment 缩写语法 */}
|
||||
<span className="icon">{icon}</span>
|
||||
<span>{text}</span>
|
||||
</>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> [!question] children 的类型为什么是 React.ReactNode 而不是 JSX.Element?
|
||||
> - `JSX.Element` 只代表 React 元素(如 `<div />`、`<Comp />`)
|
||||
> - `React.ReactNode` 范围更广:包含字符串、数字、Fragment、数组、甚至 null/undefined
|
||||
> - 💡 如果用 `JSX.Element`,当子元素是纯文本或数组时会报错
|
||||
|
||||
## React.memo —— 减少不必要的重渲染
|
||||
|
||||
```tsx
|
||||
// memo 对组件做了一层"浅比较"包装:只有 props 变化时才重新渲染
|
||||
const UserItem = React.memo(function UserItem({ name, age }: { name: string; age: number }) {
|
||||
// 即使父组件其他 state 变化,只要 name 和 age 不变,就不会重渲染
|
||||
return <li>{name},{age} 岁</li>;
|
||||
});
|
||||
|
||||
// 自定义比较函数(仅在需要深比较时使用,一般不需要)
|
||||
const compare = (prev: UserItemProps, next: UserItemProps) => prev.name === next.name && prev.age === next.age;
|
||||
const UserItemDeep = React.memo(UserItem, compare);
|
||||
```
|
||||
|
||||
> [!tip] memo 的性能权衡
|
||||
> - 适用于**频繁重渲染但 props 稳定**的叶子组件
|
||||
> - 过度使用会增加维护成本和内存开销——先用 Profile 工具定位瓶颈,再决定是否加 memo
|
||||
|
||||
## forwardRef —— 访问子组件 DOM 节点
|
||||
|
||||
```tsx
|
||||
// 父组件需要通过 ref 操作子组件内部的 DOM(如聚焦输入框)
|
||||
const FancyInput = forwardRef<HTMLInputElement, { placeholder: string }>(({ placeholder }, ref) => {
|
||||
return <input ref={ref} placeholder={placeholder} />;
|
||||
});
|
||||
|
||||
// 使用方可以拿到原生 input 的 ref
|
||||
function Form() {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus(); // 💡 挂载后自动聚焦
|
||||
}, []);
|
||||
|
||||
return <FancyInput ref={inputRef} placeholder="请输入..." />;
|
||||
}
|
||||
```
|
||||
|
||||
> [!important] forwardRef 的使用场景
|
||||
> 不是所有组件都需要 forwardRef。**优先通过 props 解决问题**(可控组件模式)。只在以下场景才需要:
|
||||
> - 需要暴露 DOM API(聚焦、测量、滚动等)
|
||||
> - 封装底层 UI 库组件时需要透传 ref
|
||||
|
||||
## 受控组件模式(Controlled Components)
|
||||
|
||||
```tsx
|
||||
// 组件的内部 state 由外部 props 控制 —— 单一数据源原则
|
||||
function ControlledInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
||||
return (
|
||||
<input
|
||||
value={value} // 🔑 value 由外部决定
|
||||
onChange={(e) => onChange(e.target.value)} // 🔑 变化时通知外部更新
|
||||
placeholder="输入内容"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 父组件持有唯一真实状态
|
||||
function SearchPage() {
|
||||
const [query, setQuery] = useState("");
|
||||
return (
|
||||
<>
|
||||
<ControlledInput value={query} onChange={setQuery} />
|
||||
<p>搜索关键词:{query || "(空)"}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> [!note] 受控 vs 非受控
|
||||
> - **受控**:state 在父组件中管理,数据流向清晰(✅ 推荐大多数场景)
|
||||
> - **非受控**:state 在子组件内部管理,通过 ref 读取(⚡ 适合简单的表单快速实现)
|
||||
|
||||
## Context 初探(轻量级全局状态)
|
||||
|
||||
Context 是 React 内置的跨组件树通信机制,适合传递那些"多处用到但不应逐层透传"的数据。
|
||||
|
||||
```tsx
|
||||
// 创建 Context,初始值通常设为占位值(实际值由 Provider 注入)
|
||||
const ThemeContext = createContext<"light" | "dark">("light");
|
||||
|
||||
function App() {
|
||||
const [theme, setTheme] = useState<"light" | "dark">("light");
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={theme}>
|
||||
{/* 其下所有子组件(不限层级)都可以通过 useContext 消费 */}
|
||||
<Toggle />
|
||||
<Content />
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Toggle() {
|
||||
const theme = useContext(ThemeContext); // 无需中间层透传
|
||||
return <button onClick={() => setTheme(t => t === "light" ? "dark" : "light")}>切换主题</button>;
|
||||
}
|
||||
```
|
||||
|
||||
> [!warning] Context 的性能陷阱
|
||||
> - Provider value 如果是**新对象**,每次渲染都是不同的引用 → 所有消费者全部重渲染
|
||||
> - ✅ 解决:用 `useReducer` 返回稳定的 `{state, dispatch}` 对象,保持引用一致
|
||||
> - ✅ 解决:拆分 Context,避免一个大 Context 塞入过多数据
|
||||
|
||||
## 组件组合的进阶模式
|
||||
|
||||
### Compound Components(复合组件)
|
||||
|
||||
当一个组件的子项之间存在**隐式共享状态**时,使用复合组件模式:
|
||||
|
||||
```tsx
|
||||
// 手风琴组件:Tab 之间共享 activeIndex,但父组件无需关心
|
||||
function Accordion({ children }: { children: React.ReactNode }) {
|
||||
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
||||
|
||||
return (
|
||||
<AccordionContext.Provider value={{ activeIndex, setActiveIndex }}>
|
||||
<div className="accordion">{children}</div>
|
||||
</AccordionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionTab({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
const ctx = useContext(AccordionContext);
|
||||
// 💡 每个 Tab 自己从 Context 里拿状态,父组件不用逐一传递
|
||||
const isActive = ctx.activeIndex !== null;
|
||||
return (
|
||||
<div onClick={() => ctx.setActiveIndex(ctx.activeIndex)}>
|
||||
<h3>{title}</h3>
|
||||
{isActive && <div>{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 对外暴露子组件属性,方便用户使用
|
||||
Accordion.Tab = AccordionTab;
|
||||
|
||||
// 使用:状态在 Tabs 间共享,父组件只需包裹
|
||||
<Accordion>
|
||||
<Accordion.Tab title="第一章">...</Accordion.Tab>
|
||||
<Accordion.Tab title="第二章">...</Accordion.Tab>
|
||||
</Accordion>
|
||||
```
|
||||
|
||||
> [!question] 什么时候用 compound components?
|
||||
> 当你的一组子组件需要**彼此感知对方的状态**,又不想让父组件来协调时。典型场景:Tabs/Accordion/SwitchGroup/Table。
|
||||
|
||||
### Composition Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Parent as 父组件
|
||||
participant Child as 子组件
|
||||
participant DOM as DOM
|
||||
|
||||
Parent->>Child: 传入 props + children(JSX 树)
|
||||
Note over Parent,Child: Props 描述配置,Children 描述内容
|
||||
Child->>DOM: 根据 props 渲染特定结构
|
||||
Child->>Child: 插入 children 到指定位置
|
||||
Note over Child: 组合 = 配置 + 内容的分离
|
||||
```
|
||||
|
||||
> [!tip] 组合 vs 继承
|
||||
> React 的设计哲学是 **组合优于继承**:
|
||||
> - JS 的 class 继承会导致紧耦合和脆弱的基类依赖
|
||||
> - React 组合让每个组件保持独立,行为通过 props/children/Callbacks 灵活拼装
|
||||
> - 💡 如果想共享逻辑,优先考虑 Custom Hooks 而非继承
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[REACT/1. 基础篇/01-环境搭建与项目结构]] — React 项目初始化与目录规范
|
||||
- [[REACT/1. 基础篇/02-JSX 语法]] — JSX 表达式、事件绑定、条件渲染
|
||||
- [[REACT/1. 基础篇/04-State 与不可变性]] — State 设计原则与更新模式
|
||||
- [[REACT/2. Hooks 篇/05-核心 Hooks]] — useState/useEffect/useContext/useRef 原理
|
||||
- [[REACT/2. Hooks 篇/07-自定义 Hooks]] — 逻辑复用与常见 Hook 封装
|
||||
- [[REACT/4. 进阶篇/11-组件通信模式]] — Props Drill / Context / 状态管理库方案对比
|
||||
- [[REACT/3. 生态工具篇/09-状态管理]] — Context API vs Zustand vs Redux Toolkit
|
||||
- [[REACT/5. 工程实践篇/15-性能优化]] — React.memo / 虚拟列表 / Code Splitting
|
||||
@@ -0,0 +1,317 @@
|
||||
---
|
||||
tags: [React, State, Immutability, Hooks, Frontend]
|
||||
create time: 2026-04-29 22:03
|
||||
---
|
||||
|
||||
# State 与不可变性
|
||||
|
||||
## 概述
|
||||
|
||||
State 是 React 组件的数据源。**不可变性**(Immutability)是 React 状态管理的基石——React 通过引用比较判断 state 是否变化,只有创建了新的引用才会触发重渲染。理解这一点,就能避免 "数据变了但 UI 没更新" 这一经典 Bug。
|
||||
|
||||
## 为什么必须遵守不可变性?
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["setState(newState)"] --> B{"old === new?"}
|
||||
B -->|不同| C["触发重渲染 ✅"]
|
||||
B -->|相同| D["跳过渲染 ❌ 数据已变但 UI 不变"]
|
||||
|
||||
style C fill:#4FC08D,color:#fff
|
||||
style D fill:#F5A87D,color:#000
|
||||
```
|
||||
|
||||
> [!question] 思考:为什么 React 用浅比较而不是深比较?
|
||||
>
|
||||
> - **性能**:深比较复杂度为 O(n),大型嵌套对象代价极高;浅比较 O(1)
|
||||
> - **职责划分**:React 负责高效检测变化,开发者负责正确生成新引用
|
||||
> - **历史教训**:Redux / MobX 等库的对比证明,强制不可变性让调试和追踪变更成为可能
|
||||
|
||||
> [!note] 核心原则
|
||||
> React 只认引用地址,不认内容。`{a: 1}` 和 `{a: 1}` 在 JS 中是两个不同的对象,`===` 为 `false`。因此只要每次创建新引用,React 就会检测到变化。
|
||||
|
||||
## useState 基础用法
|
||||
|
||||
```tsx
|
||||
const [count, setCount] = useState(0);
|
||||
const [user, setUser] = useState<{ name: string; age: number }>({ name: "", age: 0 });
|
||||
const [items, setItems] = useState<string[]>([]);
|
||||
|
||||
// setState 可以接收值或 updater 函数
|
||||
setCount(c => c + 1); // 推荐:基于旧值时用 updater,避免闭包陷阱
|
||||
```
|
||||
|
||||
### Updater 函数模式
|
||||
|
||||
当新 state 依赖旧 state 时,必须使用 updater 形式:
|
||||
|
||||
```tsx
|
||||
// 场景:快速连续点击,用值可能导致丢失更新
|
||||
handleClick = () => {
|
||||
setCount(count + 1); // ❌ 如果 count 被缓存,可能只+1
|
||||
setCount(count + 1);
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
setCount(c => c + 1); // ✅ 每次都拿到最新值,两次点击正确 +2
|
||||
setCount(c => c + 1);
|
||||
};
|
||||
```
|
||||
|
||||
> [!tip] 最佳实践
|
||||
> 养成习惯:**凡是新值依赖于旧值,一律用 updater 函数**。即使当前看起来没问题,也能预防未来重构引入的闭包问题。
|
||||
|
||||
## 不可变更新模式
|
||||
|
||||
### 数组操作
|
||||
|
||||
```tsx
|
||||
const [list, setList] = useState(["a", "b", "c"]);
|
||||
|
||||
// 添加末尾
|
||||
setList(prev => [...prev, "d"]);
|
||||
|
||||
// 插入开头
|
||||
setList(prev => ["new", ...prev]);
|
||||
|
||||
// 删除(按 index)
|
||||
setList(prev => prev.filter((_, i) => i !== targetIndex));
|
||||
|
||||
// 更新指定元素
|
||||
setList(prev => prev.map((item, i) =>
|
||||
i === targetIndex ? { ...item, completed: true } : item
|
||||
));
|
||||
|
||||
// 安全排序(sort() 原地修改!必须先拷贝)
|
||||
setList(prev => [...prev].sort((a, b) => a.id - b.id));
|
||||
```
|
||||
|
||||
> [!warning] 常见陷阱
|
||||
>
|
||||
> ```tsx
|
||||
> // ❌ sort()/reverse()/push()/splice() 都原地修改原数组!
|
||||
> list.sort(); // list 引用不变
|
||||
> setList(list); // React 跳过渲染
|
||||
>
|
||||
> // ✅ 先拷贝再操作
|
||||
> setList([...list].sort());
|
||||
> ```
|
||||
|
||||
### 对象更新
|
||||
|
||||
```tsx
|
||||
const [form, setForm] = useState({
|
||||
profile: { name: "", email: "" },
|
||||
settings: { theme: "light", notifications: true },
|
||||
});
|
||||
|
||||
// 顶层字段 —— 展开一层即可
|
||||
setForm(prev => ({ ...prev, title: "新标题" }));
|
||||
|
||||
// 嵌套一层 —— 逐层展开
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
profile: { ...prev.profile, name: "新名字" },
|
||||
}));
|
||||
```
|
||||
|
||||
> [!warning] 多层嵌套会非常冗长,这是手动管理复杂 state 的天然缺陷
|
||||
>
|
||||
> 如果你的嵌套超过两层,说明应该考虑 `useReducer` 或外部状态管理库了。
|
||||
|
||||
## 状态提升
|
||||
|
||||
> [!abstract] 什么是状态提升?
|
||||
> React 官方提出的设计模式:将共享状态移动到最先需要它的共同父组件中,子组件通过 props 读取,通过回调 prop 请求变更。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph A["状态提升前 ❌"]
|
||||
S1[CounterA 独立 state]
|
||||
S2[CounterB 独立 state]
|
||||
S3[总和无法计算]
|
||||
S1 --> S3
|
||||
S2 --> S3
|
||||
end
|
||||
|
||||
subgraph B["状态提升后 ✅"]
|
||||
P[App 持有 state]
|
||||
C1[CounterA via props]
|
||||
C2[CounterB via props]
|
||||
Total["显示总和"]
|
||||
P --> C1
|
||||
P --> C2
|
||||
P --> Total
|
||||
end
|
||||
```
|
||||
|
||||
```tsx
|
||||
// 状态提升后
|
||||
function App() {
|
||||
const [countA, setCountA] = useState(0);
|
||||
const [countB, setCountB] = useState(0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>总和: {countA + countB}</p>
|
||||
<Counter value={countA} onChange={setCountA} />
|
||||
<Counter value={countB} onChange={setCountB} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> [!info] 何时使用状态提升?
|
||||
> - 两个或多个组件需要同步同一份数据
|
||||
> - 一个组件的交互需要影响其他组件的行为
|
||||
> - 没有更好的替代方案(如 Context、状态管理库)之前,先从状态提升开始
|
||||
|
||||
## useReducer:管理复杂 state
|
||||
|
||||
当 state 逻辑变复杂时,`useReducer` 比 `useState(updater)` 更清晰:
|
||||
|
||||
```tsx
|
||||
interface State {
|
||||
items: Item[];
|
||||
status: "idle" | "loading" | "error";
|
||||
error?: string;
|
||||
}
|
||||
type Action =
|
||||
| { type: "ADD"; payload: Item }
|
||||
| { type: "SET_LOADING" }
|
||||
| { type: "SET_ERROR"; payload: string };
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case "ADD": return { ...state, items: [...state.items, action.payload] };
|
||||
case "SET_LOADING": return { ...state, status: "loading" };
|
||||
case "SET_ERROR": return { ...state, status: "error", error: action.payload };
|
||||
default: return state; // 未匹配的动作返回原 state
|
||||
}
|
||||
}
|
||||
|
||||
function MyComponent() {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{state.status === "loading" && <Spinner />}
|
||||
{state.status === "error" && <Alert>{state.error}</Alert>}
|
||||
{state.items.map(item => (
|
||||
<Item key={item.id} {...item} onRemove={() => dispatch({ type: "REMOVE", payload: item.id })} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> [!tip] useReducer 的优势
|
||||
>
|
||||
> 1. **动作语义化**:`dispatch({ type: "ADD" })` 比 `setState()` 更能表达意图
|
||||
> 2. **逻辑集中**:所有变更规则集中在 reducer 函数里,方便测试
|
||||
> 3. **批量关联更新**:一个 action 可以同时更新多个相互关联的子值
|
||||
> 4. **DevTools 友好**:配合 Redux DevTools 可实现时间旅行调试
|
||||
|
||||
### useState vs useReducer 决策表
|
||||
|
||||
| 维度 | 选 useState | 选 useReducer |
|
||||
|------|------------|--------------|
|
||||
| state 类型 | 简单标量(number, boolean, string) | object / array,结构较复杂 |
|
||||
| 下一个 state | 不依赖旧值,或直接给出 | 依赖旧值,且有分支逻辑 |
|
||||
| 变更来源 | 单一触发点 | 多个可能的 action 类型 |
|
||||
| 子值联动 | 无需联动 | 一次操作需更新多个子值 |
|
||||
| 可测试性 | 不需要单独测试 | reducer 是纯函数,可独立测试 |
|
||||
|
||||
> [!example] 同一个场景的两种写法对比
|
||||
>
|
||||
> 假设实现一个计数器,支持 increment/decrement/reset:
|
||||
>
|
||||
> ```tsx
|
||||
> // useState 版本
|
||||
> const [n, setN] = useState(0);
|
||||
> // reset 需要知道初始值,不够自包含
|
||||
>
|
||||
> // useReducer 版本
|
||||
> function reducer(state, action) {
|
||||
> switch (action.type) {
|
||||
> case "INCREMENT": return { n: state.n + 1 };
|
||||
> case "DECREMENT": return { n: state.n - 1 };
|
||||
> case "RESET": return { n: INITIAL_VALUE }; // self-contained
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
|
||||
## 什么时候不应该用 State?
|
||||
|
||||
> [!important] 误区:把所有数据都塞进 state
|
||||
>
|
||||
> 不是所有数据都需要 state。以下情况应优先考虑替代方案:
|
||||
|
||||
```tsx
|
||||
// ❌ 不该存为 state:可以从 props 或其他 state 计算得出
|
||||
const [fullName, setFullName] = useState("");
|
||||
useEffect(() => setFullName(`${firstName} ${lastName}`), [firstName, lastName]);
|
||||
|
||||
// ✅ 直接在渲染中计算
|
||||
const fullName = `${firstName} ${lastName}`;
|
||||
```
|
||||
|
||||
```tsx
|
||||
import { useRef } from "react";
|
||||
|
||||
// ❌ 不该存为 state:需要跨渲染保持可变引用(定时器 ID、DOM 节点、WebSocket)
|
||||
let timerId = useRef<NodeJS.Timeout>(); // 不能在渲染期间赋值
|
||||
|
||||
// ✅ 用 useRef 存储非渲染相关的可变值
|
||||
const timerRef = useRef<NodeJS.Timeout>();
|
||||
timerRef.current = setTimeout(() => {}, 1000);
|
||||
|
||||
// ⚠️ 注意:修改 ref.current 不会触发重渲染!
|
||||
```
|
||||
|
||||
```tsx
|
||||
import { useMemo, useCallback } from "react";
|
||||
|
||||
// ❌ 过度优化:每个小值都用 useMemo/useCallback
|
||||
const [n, setN] = useState(0);
|
||||
const doubled = useMemo(() => n * 2, [n]); // 多此一举
|
||||
|
||||
// ✅ useMemo/useCallback 只在昂贵计算或传给 memo 子组件时使用
|
||||
```
|
||||
|
||||
## 性能注意事项
|
||||
|
||||
> [!summary] 本节要点:state 的每一次 setter 调用都会触发当前组件重渲染,优化的核心思路是减少不必要的渲染和降低渲染成本。
|
||||
|
||||
1. **拆分 state** — 不相关的状态不要放在同一个 state 变量里,避免一处变更导致整个大对象所在组件全部重渲染
|
||||
|
||||
2. **惰性初始化** — `useState(() => computeExpensiveValue())` 中的工厂函数只在首次挂载时执行,适合初始化开销大的场景
|
||||
|
||||
3. **稳定引用** — 组件函数本身在每次渲染都是新引用,如需传给子组件,优先用 `useCallback`;跨渲染保存可变引用用 `useRef`
|
||||
|
||||
```tsx
|
||||
// 惰性初始化
|
||||
const [cache, setCache] = useState<Map<string, any>>(() => new Map());
|
||||
|
||||
// useRef 存储 callback(配合 memo 使用)
|
||||
const callbackRef = useRef(callback);
|
||||
callbackRef.current = callback;
|
||||
// 子组件中通过 callbackRef.current 访问
|
||||
```
|
||||
|
||||
> [!tip] React 18 批处理升级
|
||||
>
|
||||
> React 18 起,**所有状态更新都自动批处理**,包括:
|
||||
> - Promise 回调内的 setState
|
||||
> - setTimeout 内的 setState
|
||||
> - 原生事件处理器内的 setState
|
||||
>
|
||||
> 不再需要 `unstable_batchedUpdates`,也不再需要担心手动批处理的问题。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/REACT/1. 基础篇/03-组件与 Props]]
|
||||
- [[hhs/REACT/2. Hooks 篇/05-核心 Hooks]]
|
||||
- [[hhs/REACT/2. Hooks 篇/06-性能优化 Hooks]]
|
||||
- [[hhs/REACT/4. 进阶篇/11-组件通信模式]]
|
||||
- [[hhs/REACT/5. 工程实践篇/15-性能优化]]
|
||||
Reference in New Issue
Block a user