vault backup: 2026-04-29 22:28:21
This commit is contained in:
+129
-21
@@ -13,19 +13,19 @@ create time: 2026-04-29 22:08
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["需要全局状态吗?"] -->|"否"| B["useState / useReducer ✅"]
|
||||
A -->|"是"| C["状态类型?"]
|
||||
A["Do you need global state?"] -->|"No"| B["useState / useReducer ✅"]
|
||||
A -->|"Yes"| C["State type?"]
|
||||
|
||||
C --> D["纯 UI 状态<br/>(主题、菜单展开、模态框)"]
|
||||
C --> E["服务器数据 / 异步缓存"]
|
||||
C --> F["应用级业务状态<br/>(用户信息、购物车、权限)"]
|
||||
C --> D["Pure UI state<br/>(theme, menu, modal)"]
|
||||
C --> E["Server data / async cache"]
|
||||
C --> F["App-level business state<br/>(user info, cart, permissions)"]
|
||||
|
||||
D --> G["Context API ✅"]
|
||||
E --> H["TanStack Query / SWR ✅"]
|
||||
F --> I["状态规模?"]
|
||||
F --> I["State size?"]
|
||||
|
||||
I --> J["小型 (< 5 store)"]
|
||||
I --> K["中大型"]
|
||||
I --> J["Small (< 5 stores)"]
|
||||
I --> K["Medium-Large"]
|
||||
|
||||
J --> L["Zustand ✅"]
|
||||
K --> M["Redux Toolkit + RTK Query ✅"]
|
||||
@@ -62,14 +62,44 @@ function Profile() {
|
||||
|
||||
### Context 的性能局限
|
||||
|
||||
> [!question] 为什么 value 变化会导致所有消费组件重渲染?
|
||||
> 因为 Context.Provider.value 是一个引用类型。每次 `value={{ user, login }}` 都会创建一个新对象,React 比较的是引用地址而非内容——地址不同就判定为"值变了"。
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| value 变化时所有消费组件重渲染 | Context Value 引用每次都是新的 | 拆分多个 Context / 用 reducer 保持 dispatch 引用稳定 |
|
||||
| 不支持 selector | 没有 "只取子字段" 的机制 | 手动封装或使用第三方库 |
|
||||
| 不支持 selector | 没有"只取子字段"的机制 | 手动封装或使用第三方库 |
|
||||
| SSR hydration mismatch | 客户端与初始值不一致 | 延迟消费或用 useEffect 包裹 |
|
||||
|
||||
### Context 最佳实践 checklist
|
||||
|
||||
> [!tip] 使用 Context 时的关键要点
|
||||
> - ✅ 始终用 `useMemo` 包装 Provider value(防止每次渲染创建新对象)
|
||||
> - ✅ 函数型 props 用 `useCallback` 缓存(减少不必要的消费者重渲染)
|
||||
> - ✅ 将"低频更新"与"高频更新"拆分为独立 Context
|
||||
> - ❌ 避免在 Context 中存储大量频繁变化的数据(如输入框实时值)
|
||||
> - ❌ 不要把 Context 当作全局状态管理的全能替代方案
|
||||
|
||||
```tsx
|
||||
// ❌ 反模式 — value 每次都是新引用,所有 Consumer 会无条件重渲染
|
||||
function BadProvider() {
|
||||
const [user, login] = useAuth();
|
||||
return <AuthContext.Provider value={{ user, login }}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
// ✅ 正确做法 — useMemo + useCallback 双重稳定化
|
||||
function GoodProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, login] = useAuth();
|
||||
const value = useMemo(() => ({ user, login }), [user, login]);
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
```
|
||||
|
||||
## Zustand —— 轻量级现代方案
|
||||
|
||||
> [!tip] Zustand 的核心理念:像用 state 一样用 store
|
||||
> 不需要 Provider、不需要 dispatch、不需要 reducer。直接 create → 直接 use,零样板代码。
|
||||
|
||||
```ts
|
||||
import { create } from "zustand";
|
||||
|
||||
@@ -84,8 +114,10 @@ const useStore = create<StoreState>((set, get) => ({
|
||||
count: 0,
|
||||
users: [],
|
||||
|
||||
// ✅ set 接受函数形式可以拿到上一次状态 — 避免闭包陷阱
|
||||
increment: () => set(state => ({ count: state.count + 1 })),
|
||||
|
||||
// ✅ 异步操作直接在 action 里写
|
||||
fetchUsers: async () => {
|
||||
const res = await fetch("/api/users");
|
||||
const data = await res.json();
|
||||
@@ -95,15 +127,42 @@ const useStore = create<StoreState>((set, get) => ({
|
||||
|
||||
// 组件中使用
|
||||
function Counter() {
|
||||
// ✅ 只订阅 count —— 其他状态变化不会触发此组件重渲染
|
||||
// ✅ selector 模式:只订阅 count,users/increment 变化不会触发此组件重渲染
|
||||
const count = useStore(s => s.count);
|
||||
const increment = useStore(s => s.increment);
|
||||
|
||||
return <button onClick={increment}>Count: {count}</button>;
|
||||
}
|
||||
|
||||
// 批量更新
|
||||
useStore.setState(({ count }) => ({ count: count + 1, flag: true }));
|
||||
// ⚠️ setState 直接修改(不通过 hook),适用于回调和非 React 环境
|
||||
useStore.setState(({ count }) => ({ count: count + 1 }));
|
||||
```
|
||||
|
||||
### Zustand 中间件
|
||||
|
||||
> [!example] persist 中间件:自动将状态同步到 localStorage
|
||||
> 刷新页面后数据不丢失,非常适合持久化用户偏好设置。
|
||||
|
||||
```ts
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
interface ThemeState {
|
||||
mode: "light" | "dark";
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
const useThemeStore = create<ThemeState>()(
|
||||
// ✅ 多个中间件可以叠加使用
|
||||
persist(
|
||||
(set) => ({
|
||||
mode: "light",
|
||||
toggle: () => set(state => ({ mode: state.mode === "light" ? "dark" : "light" })),
|
||||
}),
|
||||
{ name: "theme-storage" }, // localStorage key
|
||||
),
|
||||
);
|
||||
// ⚠️ persist 默认只序列化为 JSON,不支持 Date / RegExp / Function 等复杂类型
|
||||
```
|
||||
|
||||
### Zustand 的优势
|
||||
@@ -118,6 +177,9 @@ useStore.setState(({ count }) => ({ count: count + 1, flag: true }));
|
||||
|
||||
## Redux Toolkit —— 企业级方案
|
||||
|
||||
> [!question] Redux Toolkit vs 旧版 Redux:为什么要用 RTK?
|
||||
> 在 Redux Toolkit 出现之前,Redux 需要写 action types、action creators、switch-case reducer——样板代码极多。RTK 通过 `createSlice` + Immer,将模板代码减少 80% 以上,同时保留 Redux 的调试能力和可预测性。
|
||||
|
||||
```ts
|
||||
import { createSlice, configureStore, useDispatch, useSelector } from "@reduxjs/toolkit";
|
||||
|
||||
@@ -130,8 +192,9 @@ const counterSlice = createSlice({
|
||||
name: "counter",
|
||||
initialState: { value: 0, status: "idle" } as CounterSlice,
|
||||
reducers: {
|
||||
incremented: state => { state.value += 1; }, // ✅ Immer:直接 mutate!
|
||||
incremented: state => { state.value += 1; }, // ✅ Immer:直接 mutate!内部会自动产生不可变更新
|
||||
fetchedAsync: {
|
||||
// ✅ extraReducers builder pattern — 类型安全的事件监听
|
||||
pending: state => { state.status = "loading"; },
|
||||
fulfilled: (state, action) => {
|
||||
state.status = "succeeded";
|
||||
@@ -147,11 +210,13 @@ export const { incremented, fetchedAsync } = counterSlice.actions;
|
||||
const store = configureStore({
|
||||
reducer: { counter: counterSlice.reducer },
|
||||
});
|
||||
// configureStore 自动集成了 devtools、redux-thunk、reducer 组合 — 不需要手写
|
||||
```
|
||||
|
||||
```tsx
|
||||
function CounterComponent() {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
// ✅ useSelector 自带 selector(浅比较)—— 只有 value 变化时才重渲染
|
||||
const count = useSelector((s: AppState) => s.counter.value);
|
||||
|
||||
return <button onClick={() => dispatch(incremented())}>{count}</button>;
|
||||
@@ -160,6 +225,9 @@ function CounterComponent() {
|
||||
|
||||
### RTK Query —— 内置数据获取
|
||||
|
||||
> [!tip] RTK Query 的定位:替代 Axios + useEffect + useState 的组合拳
|
||||
> 自动处理缓存、loading 状态、重试、增量更新——把数据获取变成声明式。
|
||||
|
||||
```ts
|
||||
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
||||
|
||||
@@ -175,6 +243,34 @@ const api = createApi({
|
||||
});
|
||||
|
||||
export const { useGetUsersQuery, useUpdateUserMutation } = api;
|
||||
// ⚠️ 导出的是 hook — 组件中直接解构使用,无需手动 dispatch
|
||||
```
|
||||
|
||||
## 状态管理问题排查流程
|
||||
|
||||
> [!question] 💡 你的应用遇到了什么问题?对照以下流程图定位根因
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Performance Issue?"] --> B["Unnecessary Re-render?"]
|
||||
A --> C["State Lost / Out-of-sync?"]
|
||||
|
||||
B --> D["Reading full Context value?"]
|
||||
D -->|"Yes"| E["Switch to selector / Split Context ✅"]
|
||||
D -->|"No"| F["useSelector missing selector arg?"]
|
||||
F -->|"Missing"| G["Change to useSelector s => s.xxx ✅"]
|
||||
F -->|"OK"| H["Check React StrictMode double-execution"]
|
||||
|
||||
C --> I["Using Context for server data?"]
|
||||
I -->|"Yes"| J["Switch to TanStack Query / RTK Query ✅"]
|
||||
I -->|"No"| K["Zustand store missing persist?"]
|
||||
K -->|"Yes"| L["Add persist middleware ✅"]
|
||||
K -->|"No"| M["Store destroyed on route change?"]
|
||||
|
||||
H --> N["Expected behavior — confirm if it causes real bugs"]
|
||||
J --> O["Cache hit — no repeated requests"]
|
||||
L --> P["Restores state after refresh"]
|
||||
M --> Q["Check app mount lifecycle"]
|
||||
```
|
||||
|
||||
## 三框架横向对比
|
||||
@@ -194,20 +290,32 @@ export const { useGetUsersQuery, useUpdateUserMutation } = api;
|
||||
> [!warning] 以下做法应避免
|
||||
|
||||
```tsx
|
||||
// ❌ 把所有东西塞进一个 store
|
||||
// ❌ 把所有东西塞进一个 store — 导致 selector 粒度太粗、难以维护
|
||||
const useBigStore = create(() => ({
|
||||
user: ..., theme: ..., sidebarOpen: ..., notifications: ..., cart: ..., preferences: ..., // 20+ 个状态
|
||||
}));
|
||||
|
||||
// ✅ 按功能拆分为多个 store
|
||||
// ✅ 按功能拆分为多个 store — 职责单一,selector 精确
|
||||
const useUserStore = create(...)
|
||||
const useThemeStore = create(...)
|
||||
|
||||
// ❌ 在 state 中存储服务器返回的数据却不做缓存
|
||||
const [data, setData] = useState(fetch(...)) // 页面切换丢失,重复请求
|
||||
|
||||
// ✅ 使用 TanStack Query 等专用数据获取库处理缓存和失效
|
||||
const { data } = useQuery({ queryKey: ["users"], queryFn: fetchUsers });
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ❌ 在 state 中存储服务器返回的数据却不做缓存
|
||||
const [data, setData] = useState(fetch(...)) // 页面切换丢失,重复请求
|
||||
// 问题:每次切到同一页面都会重新请求,且没有 loading / error 状态管理
|
||||
|
||||
// ✅ 使用 TanStack Query 等专用数据获取库处理缓存和失效
|
||||
const { data, isLoading, error } = useQuery({ queryKey: ["users"], queryFn: fetchUsers });
|
||||
```
|
||||
|
||||
> [!tip] 📋 状态管理最佳实践总结
|
||||
> 1. **职责分离原则** — UI 状态用 useState,全局状态用 Store,服务器数据用 Query
|
||||
> 2. **选择最小可行方案** — 能用 Context 解决的问题,不要引入 Zustand;能用 Zustand 的,不要上 Redux
|
||||
> 3. **按需订阅** — 无论哪个库,尽量用 selector 精确订阅,避免大面积重渲染
|
||||
> 4. **类型先行** — TypeScript 项目中,先定义 State interface 再写 Store,让编译器帮你守住边界
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[3. 生态工具篇/08-路由管理]] — 路由与状态的关系:URL 也是状态的一种表现形式
|
||||
- [[3. 生态工具篇/10-TS + React]] — TypeScript 类型定义在 Store 设计中的最佳实践
|
||||
|
||||
Reference in New Issue
Block a user