Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← Web 开发 / Web Development

React 生态 / React Ecosystem

1. React 生态知识体系 / React Ecosystem Knowledge System

2. React 组件渲染、协调与数据流 / React Components, Rendering, and Data Flow

3. Hooks 深度剖析:状态、副作用与并发模式 / Hooks Deep Dive: State, Effects, and Concurrency

4. 路由、表单与组件架构 / Routing, Forms, and Component Architecture

5. React 状态管理:Redux Toolkit 与 Zustand / React State Management: Redux Toolkit and Zustand

6. Next.js App Router:路由、渲染与工程边界 / Next.js App Router, Routing, Rendering, and Engineering Boundaries

7. Next.js 数据获取、缓存与变更 / Next.js Data Fetching, Caching, and Mutations

8. TanStack Query 与服务端状态管理 / TanStack Query and Server State Management

9. React 测试、性能与生产工程 / React Testing, Performance, and Production Engineering

本页目录

React 状态管理:Redux Toolkit 与 Zustand / React State Management: Redux Toolkit and Zustand ​

📅 创建时间:2026-07-28 🏷️ 标签:#Redux #Zustand #StateManagement #RTKQuery #Jotai 📚 前置知识:[[02-hooks-state-and-effects]]


📋 本章目标 ​

  • 理解 Redux 单向数据流的完整链路:Action → Dispatch → Reducer → Store → Selector
  • 掌握 Redux Toolkit 核心 API:createSlice (Immer 集成)、createAsyncThunk、RTK Query
  • 掌握 Zustand 核心用法:create 函数、选择器与浅比较、常用中间件
  • 理解 Jotai 原子化状态模型及其与 Zustand/Redux 的定位差异
  • 建立"按状态生命周期选择方案"的决策框架
  • 识别并避免状态管理的 6 类常见反模式

第1部分:状态分类 —— 在选库之前 ​

1.1 状态管理的第一步不是选库 ​

大多数团队的问题是:先选了一个库(通常是 Redux),然后把所有东西往里塞。正确的顺序是:先分类,再选方案。

┌─────────────────────────────────────────────────────────────┐
│                    状态四象限分类法                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│            生命周期短              生命周期长               │
│       ┌──────────┬──────────┐ ┌──────────┬──────────┐     │
│       │ 弹窗开关  │ 表单草稿  │ │ 用户偏好  │ 编辑器    │     │
│  组件  │ 展开折叠  │ 输入校验  │ │ 主题语言  │ 文档快照  │     │
│  作用域│ 悬停态    │ 上传进度  │ │ 认证令牌  │ 多步骤    │     │
│       │          │          │ │          │ 向导      │     │
│       ├──────────┼──────────┤ ├──────────┼──────────┤     │
│       │ URL 参数  │ 列表数据  │ │ 购物车    │ WebSocket │     │
│  跨组件│ 筛选条件  │ 详情缓存  │ │ 通知列表  │ 实时协作  │     │
│  共享  │ 分页页码  │ 搜索结果  │ │ 全局消息  │ 状态      │     │
│       │ 排序字段  │          │ │          │          │     │
│       └──────────┴──────────┘ └──────────┴──────────┘     │
│                                                             │
│  原则:让状态离使用它的地方尽可能近                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

1.2 六种状态类型与推荐归宿 ​

类型典型示例推荐方案判断依据
局部 UI 状态modal open、tab index、input valueuseState仅一个组件关心
可分享导航状态filter、page、sort、searchURL searchParams需要前进/后退、可书签
表单工作状态draft values、validation errors表单库内部 (React Hook Form)高频变化、提交流
服务端缓存user list、order detail、productTanStack Query / RTK Query / SWR真实数据在服务端
会话摘要currentUser、theme、localeContext 或小型 Zustand store低频更新、全局消费
跨页面业务状态编辑器文档、多步向导、工作台布局Zustand / Redux Toolkit复杂交互、多组件协作

第2部分:Redux 数据流 —— 单向循环详解 ​

2.1 核心概念映射 ​

Redux 的核心思想可以用一句话概括:整个应用的状态储存在单一 Store 中,改变状态的唯一方式是派发 Action,Reducer 根据 Action 计算新状态。

┌─────────────────────────────────────────────────────────────┐
│                   Redux 单向数据流循环                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌──────────┐      dispatch(action)      ┌────────────┐   │
│   │          │ ─────────────────────────→ │            │   │
│   │  View    │                            │  Store     │   │
│   │ (React   │                            │  (单一状态  │   │
│   │  组件)   │ ←───────────────────────── │   树)      │   │
│   │          │    useSelector() 订阅       │            │   │
│   └──────────┘                            └─────┬──────┘   │
│        │                                       │           │
│        │ 用户点击 / 生命周期 / 网络响应          │           │
│        ▼                                       ▼           │
│   ┌──────────┐      action + old state     ┌────────────┐   │
│   │  Action  │ ──────────────────────────→ │  Reducer   │   │
│   │ Creator  │                             │  (纯函数)   │   │
│   │          │                             │            │   │
│   │ { type,  │                             │ (state,    │   │
│   │  payload }│                            │  action)   │   │
│   └──────────┘                             │ => newState│   │
│                                            └────────────┘   │
│                                                             │
│  关键约束:                                                  │
│  • Reducer 必须是纯函数 —— 相同输入必得相同输出              │
│  • State 不可直接修改 —— 必须返回新对象                      │
│  • Action 是描述"发生了什么"的普通对象                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

2.2 原始 Redux vs Redux Toolkit ​

原始 Redux 需要手写 action types、action creators、reducer switch-case,样板代码极多。Redux Toolkit (RTK) 通过 createSlice 一举解决:

问题原始 ReduxRedux Toolkit
Action type 定义手写字符串常量 const ADD = 'todos/add'createSlice 自动生成
Action creator手写函数 function add(text) { return { type: ADD, payload: text } }自动生成,同名导出
Immutable 更新手写展开运算符 [...state, newItem]Immer 内置,直接 "mutate"
Reducer 结构switch-case,容易遗漏 default对象方法映射,类型安全
Store 配置手写 createStore + combineReducers + middlewareconfigureStore 一行搞定

2.3 createSlice 深度解析 ​

ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

// ── 1. 定义 State 类型 ──
interface EditorState {
  documentId: string | null
  content: string
  isDirty: boolean
  selectedBlockIds: string[]
  undoStack: string[]
  redoStack: string[]
}

// ── 2. 初始状态 ──
const initialState: EditorState = {
  documentId: null,
  content: '',
  isDirty: false,
  selectedBlockIds: [],
  undoStack: [],
  redoStack: [],
}

// ── 3. 创建 Slice ──
const editorSlice = createSlice({
  name: 'editor',           // 自动作为 action type 前缀 "editor/..."
  initialState,
  reducers: {
    // Immer 让你直接"修改" state —— 实际生成不可变更新
    documentOpened(state, action: PayloadAction<{ id: string; content: string }>) {
      state.documentId = action.payload.id
      state.content = action.payload.content
      state.isDirty = false
      state.undoStack = []
      state.redoStack = []
    },

    contentChanged(state, action: PayloadAction<string>) {
      state.undoStack.push(state.content)   // "mutate" 但 Immer 处理为不可变
      state.content = action.payload
      state.isDirty = true
      state.redoStack = []
    },

    blocksSelected(state, action: PayloadAction<string[]>) {
      state.selectedBlockIds = action.payload
    },

    undo(state) {
      const previous = state.undoStack.pop()
      if (previous !== undefined) {
        state.redoStack.push(state.content)
        state.content = previous
      }
    },

    redo(state) {
      const next = state.redoStack.pop()
      if (next !== undefined) {
        state.undoStack.push(state.content)
        state.content = next
      }
    },

    documentSaved(state) {
      state.isDirty = false
    },
  },
})

// ── 4. 自动生成的 Action Creators 和 Reducer ──
export const {
  documentOpened,
  contentChanged,
  blocksSelected,
  undo,
  redo,
  documentSaved,
} = editorSlice.actions

export default editorSlice.reducer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

2.4 configureStore 与 Selector ​

ts
import { configureStore } from '@reduxjs/toolkit'
import { useSelector, useDispatch, TypedUseSelectorHook } from 'react-redux'
import editorReducer from './editorSlice'

// ── Store 配置 ──
export const store = configureStore({
  reducer: {
    editor: editorReducer,
    // 其他 slice 在这里组合
  },
  // middleware 自动包含 redux-thunk + devtools
  // 无需手动添加
})

// ── 类型导出(整个应用使用) ──
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

// ── 类型安全的 hooks ──
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
tsx
// ── 组件中使用 ──
function EditorToolbar() {
  const dispatch = useAppDispatch()
  const isDirty = useAppSelector(state => state.editor.isDirty)
  const selectedCount = useAppSelector(state => state.editor.selectedBlockIds.length)

  return (
    <div className="toolbar">
      <span>{selectedCount} blocks selected</span>
      <button onClick={() => dispatch(undo())} disabled={/* ... */}>Undo</button>
      <button onClick={() => dispatch(redo())} disabled={/* ... */}>Redo</button>
      <button onClick={() => dispatch(documentSaved())} disabled={!isDirty}>
        {isDirty ? 'Save*' : 'Saved'}
      </button>
    </div>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

2.5 createAsyncThunk —— 异步逻辑 ​

当数据来自服务端时,优先考虑 RTK Query(见 2.6);但当异步结果需要直接写入 Redux state(如文件上传、WebSocket 消息),createAsyncThunk 是正确工具。

ts
import { createAsyncThunk } from '@reduxjs/toolkit'

// ── Thunk 定义 ──
export const exportDocument = createAsyncThunk(
  'editor/exportDocument',
  async (format: 'pdf' | 'html', { getState, rejectWithValue }) => {
    try {
      const state = getState() as RootState
      const response = await fetch('/api/export', {
        method: 'POST',
        body: JSON.stringify({
          content: state.editor.content,
          format,
        }),
      })
      if (!response.ok) {
        return rejectWithValue(await response.text())
      }
      return await response.blob()
    } catch (err) {
      return rejectWithValue(err instanceof Error ? err.message : 'Export failed')
    }
  }
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

createAsyncThunk 自动生成三个 action:pending、fulfilled、rejected,可在 extraReducers 中响应:

ts
const editorSlice = createSlice({
  name: 'editor',
  initialState,
  reducers: { /* 同步 reducers */ },
  extraReducers: (builder) => {
    builder
      .addCase(exportDocument.pending, (state) => {
        // 可以加 exporting 标记
      })
      .addCase(exportDocument.fulfilled, (state) => {
        state.isDirty = false
      })
      .addCase(exportDocument.rejected, (state, action) => {
        console.error('Export failed:', action.payload)
      })
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

2.6 RTK Query —— 服务端缓存层 ​

RTK Query 是 RTK 内置的数据获取与缓存方案,解决了手写 loading/error/data reducer 的所有痛点。

┌─────────────────────────────────────────────────────────────┐
│                  RTK Query 缓存生命周期                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────┐    请求     ┌──────────┐    缓存命中          │
│  │ 组件挂载  │ ─────────→ │ API 调用  │ ─────────→ 返回数据  │
│  │ useXxx   │            │          │                      │
│  └──────────┘            └──────────┘                      │
│       ↑                                                    │
│       │ 组件卸载 60s 后                                     │
│       │                                                    │
│  ┌────┴─────────┐    ┌──────────────┐    ┌──────────────┐ │
│  │ 缓存订阅计数  │    │ keepUnusedData│   │ refetchOnMount│ │
│  │ subscription │    │ For: 60s      │   │ OrArgChange   │ │
│  │ === 0 时启动  │    │               │   │               │ │
│  │ 垃圾回收倒计时 │    │ 计时结束 → 清理 │   │ 新参数 → 重新 │ │
│  └──────────────┘    └──────────────┘    │ 请求          │ │
│                                          └──────────────┘ │
│                                                             │
│  Tag-based 失效(mutation 完成后自动重新获取):               │
│  ┌──────────┐    invalidatesTags    ┌──────────────────┐   │
│  │  POST    │ ───────────────────→  │ GET /api/posts    │   │
│  │ 创建文章  │     ['Posts']        │ 自动重新请求       │   │
│  └──────────┘                      └──────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

// ── API Slice 定义 ──
export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Post', 'User'],
  endpoints: (builder) => ({
    // Query: 读取数据(GET)
    getPosts: builder.query<Post[], { page: number }>({
      query: ({ page }) => `posts?page=${page}&limit=20`,
      providesTags: (result) =>
        result
          ? [...result.map(({ id }) => ({ type: 'Post' as const, id })), 'Post']
          : ['Post'],
    }),

    getPost: builder.query<Post, string>({
      query: (id) => `posts/${id}`,
      providesTags: (_result, _error, id) => [{ type: 'Post', id }],
    }),

    // Mutation: 修改数据(POST/PUT/PATCH/DELETE)
    createPost: builder.mutation<Post, Omit<Post, 'id'>>({
      query: (body) => ({ url: 'posts', method: 'POST', body }),
      invalidatesTags: ['Post'],   // 创建后使 Post 列表缓存全部失效
    }),

    updatePost: builder.mutation<Post, Partial<Post> & { id: string }>({
      query: ({ id, ...body }) => ({ url: `posts/${id}`, method: 'PATCH', body }),
      invalidatesTags: (_result, _error, { id }) => [{ type: 'Post', id }],
    }),
  }),
})

export const {
  useGetPostsQuery,
  useGetPostQuery,
  useCreatePostMutation,
  useUpdatePostMutation,
} = api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
tsx
// ── 组件中使用 ──
function PostList() {
  const [page, setPage] = useState(1)
  const { data, isLoading, isError, error, isFetching } = useGetPostsQuery({ page })

  if (isLoading) return <Skeleton />
  if (isError) return <ErrorBanner message={error} />

  return (
    <div>
      {isFetching && <ProgressBar />}  {/* 后台重新获取时不阻塞 UI */}
      {data?.map(post => <PostCard key={post.id} post={post} />)}
      <Pagination page={page} onChange={setPage} />
    </div>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

RTK Query 的核心价值:

  • 零手写 reducer:loading、error、data、refetch 全部内置
  • 智能缓存:keepUnusedDataFor 控制未使用数据的保留时间
  • 自动去重:相同参数的并发请求自动合并为一次
  • 标签失效:mutation 后通过 invalidatesTags 精确控制哪些 query 需要重新获取
  • 乐观更新:onQueryStarted 中先更新 UI 再等待服务器确认

第3部分:Redux 架构原则与边界 ​

3.1 Slice 设计原则 ​

┌─────────────────────────────────────────────────────────────┐
│                  Slice 按业务能力拆分                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ❌ 按页面控件拆分:                                        │
│     headerSlice、sidebarSlice、modalSlice、tableSlice       │
│     → 控件只是 UI 呈现,不是业务概念                         │
│                                                             │
│  ✅ 按业务能力拆分:                                        │
│     authSlice      —— 认证、登录、权限                      │
│     editorSlice    —— 编辑器文档、选区、撤销                │
│     projectSlice   —— 项目列表、当前项目、成员              │
│     notificationSlice —— 通知消息、已读状态                  │
│                                                             │
│  原则:每个 Slice 对应一个"如果没有它,应用就少了               │
│        一个功能领域"的业务概念                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

3.2 Action 命名与 Selector 封装 ​

ts
// ── Action 命名:描述"发生了什么"(领域事件),而非"做什么"(setter) ──

// ❌ 命令式 setter 命名
setTitle, setContent, toggleSidebar, incrementCount

// ✅ 领域事件命名
documentTitleEdited, documentContentUpdated, sidebarToggledByUser,
itemAddedToCart, paymentSubmitted

// ── Selector:封装派生逻辑 ──
// ❌ 在组件中计算
function Component() {
  const items = useAppSelector(s => s.cart.items)
  const total = items.reduce((sum, i) => sum + i.price * i.qty, 0)  // 每次渲染都算
}

// ✅ 封装进 selector(可用 reselect 做记忆化)
import { createSelector } from '@reduxjs/toolkit'

const selectCartItems = (state: RootState) => state.cart.items
export const selectCartTotal = createSelector(selectCartItems, (items) =>
  items.reduce((sum, i) => sum + i.price * i.qty, 0)
)

function Component() {
  const total = useAppSelector(selectCartTotal)  // items 不变就不重新计算
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

3.3 Redux 边界清单 ​

  • State 保持可序列化:不要放入 DOM 节点、Promise、类实例、函数
  • 异步远程数据优先 RTK Query,而不是手写 loading/error reducer
  • Middleware 用于横切关注点(日志、埋点、崩溃报告),不放业务逻辑
  • 不在 Redux 中缓存服务端数据(那是 RTK Query 的职责)
  • DevTools 是架构审视工具,不只是调试工具——看 action 序列能发现设计问题

第4部分:Zustand —— 轻量级状态管理 ​

4.1 核心理念 ​

Zustand 的核心哲学:一个 store 就是一个 hook。没有 Provider、没有 context、没有 action type 字符串、没有 reducer switch-case。

┌─────────────────────────────────────────────────────────────┐
│                  Zustand vs Redux 概念对比                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Redux Toolkit              │  Zustand                      │
│  ──────────────────────────┼────────────────────────────── │
│  createSlice + configure   │  create( ) — 一个函数搞定     │
│  Provider 包裹根组件        │  无需 Provider                 │
│  dispatch(action) 触发     │  直接调用 store 上的方法       │
│  useSelector 读取          │  useStore(selector) 读取       │
│  Immer 内置               │  Immer 通过中间件可选           │
│  Redux DevTools            │  devtools 中间件               │
│  createAsyncThunk          │  直接写 async 函数              │
│  RTK Query                 │  配合 TanStack Query 使用       │
│                                                             │
│  共同点:都基于"不可变状态 + 选择器订阅"模式                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

4.2 create 函数基础 ​

ts
import { create } from 'zustand'

// ── 定义 Store 类型 ──
interface BearStore {
  bears: number
  fish: number
  addBear: () => void
  eatFish: () => void
  reset: () => void
}

// ── 创建 store(就是一个 hook) ──
export const useBearStore = create<BearStore>((set) => ({
  bears: 0,
  fish: 100,

  addBear: () => set((state) => ({ bears: state.bears + 1 })),

  eatFish: () => set((state) => ({
    fish: state.fish - 1,
    bears: state.bears + 1,
  })),

  reset: () => set({ bears: 0, fish: 100 }),
}))

// ── 组件使用:无需 Provider ──
function BearCounter() {
  const bears = useBearStore((state) => state.bears)
  return <h1>{bears} bears around here</h1>
}

function Controls() {
  const addBear = useBearStore((state) => state.addBear)
  const eatFish = useBearStore((state) => state.eatFish)
  return (
    <>
      <button onClick={addBear}>Add bear</button>
      <button onClick={eatFish}>Eat fish</button>
    </>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

4.3 选择器与性能 ​

Zustand 的渲染优化核心在于选择器的精确度。如果选择器返回的值和上次相同(通过 Object.is 或 shallow 比较),组件不重渲染。

ts
// ── 选择器粒度与重渲染 ──

// ❌ 订阅整个 store —— 任何字段变化都会重渲染
const state = useBearStore()                     // 强烈不推荐

// ✅ 精确选择器 —— 只在 bears 变化时重渲染
const bears = useBearStore((state) => state.bears)

// ✅ 组合选择器 —— 浅比较(shallow equality)
import { shallow } from 'zustand/shallow'

const { bears, fish } = useBearStore(
  (state) => ({ bears: state.bears, fish: state.fish }),
  shallow   // { bears: 0, fish: 100 } 与上次浅比较
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─────────────────────────────────────────────────────────────┐
│              选择器重渲染决策流程                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  选择器返回新值                                              │
│      │                                                      │
│      ▼                                                      │
│  ┌─────────────────┐    是     ┌─────────────────┐         │
│  │ 无 equalityFn?   │ ──────→ │ Object.is 比较   │         │
│  └─────────────────┘          │ 相同?→ 不重渲染 │         │
│      │ 否                     │ 不同?→ 重渲染   │         │
│      ▼                       └─────────────────┘         │
│  ┌─────────────────┐                                       │
│  │ shallow?        │ → 浅比较顶层属性                       │
│  │ 相同?→ 不重渲染 │                                       │
│  │ 不同?→ 重渲染   │                                       │
│  └─────────────────┘                                       │
│      │                                                      │
│  ┌───┴─────────────┐                                       │
│  │ 自定义比较函数    │ → 完全控制比较逻辑                     │
│  │ (state, prev)   │                                       │
│  └─────────────────┘                                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

4.4 常用中间件 ​

Zustand 的中间件通过 create 的管道组合,类似 Express/Koa 的模式。

ts
import { create } from 'zustand'
import { persist, devtools, subscribeWithSelector } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'

interface SettingsStore {
  theme: 'light' | 'dark'
  fontSize: number
  language: string
  setTheme: (theme: 'light' | 'dark') => void
  setFontSize: (size: number) => void
}

export const useSettingsStore = create<SettingsStore>()(
  // 中间件从内到外包裹:immer → persist → devtools → subscribeWithSelector
  devtools(
    persist(
      subscribeWithSelector(
        immer((set) => ({
          theme: 'light',
          fontSize: 16,
          language: 'zh-CN',

          setTheme: (theme) => set((state) => {
            state.theme = theme   // immer 中间件允许直接 mutate
          }),

          setFontSize: (fontSize) => set((state) => {
            state.fontSize = fontSize
          }),
        }))
      ),
      {
        name: 'settings-storage',          // localStorage key
        partialize: (state) => ({           // 只持久化部分字段
          theme: state.theme,
          fontSize: state.fontSize,
          language: state.language,
        }),
        version: 1,                         // Schema 版本
        migrate: (persisted, version) => {  // 迁移策略
          if (version === 0) {
            return { ...persisted as object, language: 'en' }
          }
          return persisted as typeof state
        },
      }
    ),
    { name: 'SettingsStore' }              // DevTools 中的显示名
  )
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

中间件功能速查:

中间件功能使用场景
persist自动同步到 localStorage / sessionStorage / AsyncStorage主题、语言、用户偏好
devtools连接 Redux DevTools,支持 action 名称和时间旅行开发和调试
immer允许在 set 中直接修改 state深层嵌套状态更新
subscribeWithSelector订阅 store 中特定字段的变化外部系统、WebSocket 回调

4.5 异步操作 ​

Zustand 不需要 createAsyncThunk。直接在 set 中写 async/await:

ts
interface FishStore {
  fish: { id: string; name: string }[]
  loading: boolean
  error: string | null
  fetchFish: () => Promise<void>
}

export const useFishStore = create<FishStore>((set) => ({
  fish: [],
  loading: false,
  error: null,

  fetchFish: async () => {
    set({ loading: true, error: null })           // 同步 set — 立即更新 loading
    try {
      const res = await fetch('/api/fish')
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      const fish = await res.json()
      set({ fish, loading: false })
    } catch (err) {
      set({ error: (err as Error).message, loading: false })
    }
  },
}))

// 组件中
function FishList() {
  const { fish, loading, error, fetchFish } = useFishStore()
  // 无需 dispatch、无需 thunk、无需 extraReducers
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

4.6 Store 外部访问与订阅 ​

Redux 需要 store.getState() + store.subscribe()。Zustand 同样支持在 React 组件树之外访问状态:

ts
// 在非 React 代码(WebSocket handler、axios interceptor 等)中
const currentFish = useFishStore.getState().fish      // 获取当前值
useFishStore.setState({ loading: true })               // 直接设置
const unsub = useFishStore.subscribe(
  (state) => state.error,
  (error) => { if (error) showToast(error) }
)                                                      // 订阅特定字段
1
2
3
4
5
6
7

第5部分:Jotai —— 原子化状态简介 ​

5.1 原子模型 vs Store 模型 ​

Jotai 从 Recoil 汲取灵感,采用自底向上的原子化状态模型。每个状态单元是一个独立的 atom,原子可以组合派生。

┌─────────────────────────────────────────────────────────────┐
│              Store 模型 vs 原子模型的架构差异                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Store 模型(Zustand / Redux)         原子模型(Jotai)     │
│  ┌─────────────────────┐               ┌──┐ ┌──┐ ┌──┐     │
│  │    单一 Store 树     │               │a1│ │a2│ │a3│     │
│  │  ┌────┬────┬────┐   │               └─┬┘ └─┬┘ └─┬┘     │
│  │  │auth│cart│edit│   │                 │    │    │       │
│  │  └────┴────┴────┘   │                 ▼    ▼    ▼       │
│  └─────────────────────┘              ┌────┬────┬────┐     │
│      组件 A 订阅 auth                     │ 组件 A  │       │
│      组件 B 订阅 cart                     │(订阅 a1,│       │
│      组件 C 订阅 edit                     │  a2)    │       │
│                                         └─────────┘       │
│  状态集中管理        │  状态分散定义,按需组合               │
│  适合全局状态        │  适合组件级状态共享                   │
│  有明确的边界        │  边界模糊,但极其灵活                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

5.2 基本用法 ​

tsx
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'

// ── 基础原子 ──
const countAtom = atom(0)
const nameAtom = atom('World')

// ── 持久化原子 ──
const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light')

// ── 派生原子(computed / derived) ──
const doubleCountAtom = atom((get) => get(countAtom) * 2)
const greetingAtom = atom((get) => `Hello, ${get(nameAtom)}!`)

// ── 可写派生原子 ──
const celsiusAtom = atom(0)
const fahrenheitAtom = atom(
  (get) => get(celsiusAtom) * 9 / 5 + 32,        // getter
  (_get, set, newF: number) => {                  // setter
    set(celsiusAtom, (newF - 32) * 5 / 9)
  }
)

// ── 组件使用 ──
function Counter() {
  const [count, setCount] = useAtom(countAtom)          // 读写
  const double = useAtomValue(doubleCountAtom)          // 只读
  const setTheme = useSetAtom(themeAtom)                // 只写
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

5.3 Jotai 的定位 ​

Jotai 在状态图谱中的独特位置:

场景ZustandJotai说明
全局配置(theme, locale)很适合可以Zustand 更自然
复杂表单状态共享可以很适合Jotai 原子可以独立存在
编辑器文档很适合可以Zustand 的不可变更适合复杂状态
多个独立 UI 状态需要放一个 store很适合各原子互不干扰
派生计算需要 selector天然支持Jotai 的 atom(get => ...) 自动追踪依赖
服务端缓存配合 TanStack Query配合 TanStack Query两者都不是服务端缓存方案

一句话:Zustand 是"一个 store 管一件事",Jotai 是"一个 atom 管一个值"。


第6部分:状态方案选型决策树 ​

6.1 决策框架 ​

┌─────────────────────────────────────────────────────────────┐
│                  状态方案选型决策树                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│                    ┌─────────────────┐                      │
│                    │ 状态的生命周期? │                      │
│                    └────────┬────────┘                      │
│                             │                               │
│              ┌──────────────┼──────────────┐                │
│              ▼              ▼              ▼                │
│       单次渲染/事件   单次组件挂载     跨多个组件              │
│              │              │              │                │
│              ▼              ▼              ▼                │
│        ┌──────────┐  ┌──────────┐  ┌──────────────┐       │
│        │  useRef  │  │ useState │  │ 是否可放入URL? │       │
│        │ 或局部变量 │  │useReducer│  └──────┬───────┘       │
│        └──────────┘  └──────────┘         │               │
│                                    ┌──────┴──────┐        │
│                                    ▼             ▼        │
│                                 可以        不可以          │
│                                    │             │        │
│                                    ▼             ▼        │
│                              ┌──────────┐ ┌──────────────┐│
│                              │ search   │ │ 来自服务端?   ││
│                              │ Params   │ └──────┬───────┘││
│                              └──────────┘        │        ││
│                                          ┌───────┴───────┐││
│                                          ▼               ▼││
│                                         是              否 ││
│                                          │               │ ││
│                                          ▼               ▼ ││
│                                   ┌────────────┐ ┌────────┐│
│                                   │ TanStack   │ │复杂度? ││
│                                   │ Query /    │ └───┬────┘│
│                                   │ RTK Query  │     │     │
│                                   │ / SWR      │  ┌──┴──┐ │
│                                   └────────────┘  ▼     ▼ │
│                                              低复杂度 高复杂│
│                                                  │     │   │
│                                                  ▼     ▼   │
│                                           ┌────────┐┌─────┐│
│                                           │Zustand ││Redux│││
│                                           │/Jotai  ││Tool-│││
│                                           │/Context││kit  │││
│                                           └────────┘└─────┘│
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

6.2 各方案的"甜点区" ​

ts
// ── 场景1:组件自身使用 ──
function Accordion() {
  const [open, setOpen] = useState(false)
  // 不需要任何库
}

// ── 场景2:父子组件间传递,可放入 URL ──
function SearchPage() {
  const [searchParams, setSearchParams] = useSearchParams()
  const query = searchParams.get('q') ?? ''
  const page = Number(searchParams.get('page') ?? '1')
  // URL 是天然的可分享、可前进后退的状态容器
}

// ── 场景3:服务端数据 ──
function UserList() {
  const { data } = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
  // TanStack Query 处理 loading/error/cache/refetch/stale
}

// ── 场景4:低频跨层依赖 ──
const ThemeContext = createContext<'light' | 'dark'>('light')
// Context 适合主题、语言、用户信息等"全局常量级"状态

// ── 场景5:中等复杂度的跨组件状态 ──
const useCartStore = create<CartStore>((set) => ({ /* ... */ }))
// Zustand:轻量、无 boilerplate、选择器精确

// ── 场景6:复杂业务流程、团队协作、审计需求 ──
const store = configureStore({ reducer: { /* auth, editor, project, ... */ } })
// Redux Toolkit:强约束、DevTools、middleware、可预测
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

6.3 Context 的正确使用边界 ​

Context 最容易被误用为状态管理工具。它的设计初衷是依赖注入,不是高性能状态分发。

┌─────────────────────────────────────────────────────────────┐
│              Context 何时用 / 何时不用                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ✅ 适合 Context:                                          │
│  • 低频更新的全局配置(主题、locale、功能开关)               │
│  • 组件树的依赖注入(如 <DialogProvider>)                   │
│  • 值在应用生命周期内几乎不变                                 │
│                                                             │
│  ❌ 不适合 Context(改用 Zustand / Jotai):                 │
│  • 高频更新的状态(每秒多次)                                │
│  • 状态使用者分散在组件树各处                                │
│  • 某个深层子组件只需要状态的一个字段                        │
│  → Context 值变化会导致所有消费者及其子树全部重渲染           │
│                                                             │
│  根本原因:                                                  │
│  Context 的重渲染机制是"值变化 → 所有消费者重渲染",          │
│  没有选择器订阅的粒度控制。Zustand/Redux 的选择器              │
│  只在"组件关心的那部分状态变化"时才触发重渲染。               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
tsx
// ── 反模式:用 Context 做高频状态 ──
const MouseContext = createContext({ x: 0, y: 0 })

function MouseProvider({ children }: { children: ReactNode }) {
  const [pos, setPos] = useState({ x: 0, y: 0 })
  useEffect(() => {
    const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY })
    window.addEventListener('mousemove', handler)
    return () => window.removeEventListener('mousemove', handler)
  }, [])
  return <MouseContext.Provider value={pos}>{children}</MouseContext.Provider>
}
// 每次鼠标移动,所有消费者全部重渲染 —— 性能灾难

// ── 正确做法:用 Zustand ──
const useMouseStore = create<{ x: number; y: number }>(() => ({ x: 0, y: 0 }))
// 在 useEffect 中调用 useMouseStore.setState(...)
// 组件 useMouseStore(s => s.x) 只在 x 变化时重渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

第7部分:常见反模式与纠正 ​

7.1 反模式 1:把所有数据放入 Redux ​

┌─────────────────────────────────────────────────────────────┐
│  症状:Redux store 中有 formDraft、searchQuery、modalOpen、  │
│        ui.loading、page.scrollPosition ...                  │
│                                                             │
│  后果:                                                     │
│  • Store 膨胀,类型定义失控                                 │
│  • 组件卸载后状态残留,需要手动清理                          │
│  • 序列化开销增大,DevTools 不可用                           │
│                                                             │
│  纠正:                                                     │
│  • 回答"这个状态的 owner 是谁?生命周期多长?"               │
│  • 表单 → 表单库内部,只提交最终结果                         │
│  • 筛选/排序/分页 → URL searchParams                        │
│  • 弹窗/展开 → 组件内部 useState                            │
│  • 服务端数据 → RTK Query / TanStack Query                   │
│  • 只有真正的"跨页面业务状态"才进 Redux                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

7.2 反模式 2:在 Redux 中手写缓存服务端数据 ​

ts
// ── 反模式:手写 loading/error/data 三元组 ──
interface UsersState {
  users: User[]
  loading: boolean
  error: string | null
}

const usersSlice = createSlice({
  name: 'users',
  initialState: { users: [], loading: false, error: null } as UsersState,
  reducers: {
    fetchUsersStart(state) { state.loading = true; state.error = null },
    fetchUsersSuccess(state, action: PayloadAction<User[]>) {
      state.users = action.payload; state.loading = false
    },
    fetchUsersFailure(state, action: PayloadAction<string>) {
      state.error = action.payload; state.loading = false
    },
  },
})

// 问题:
// 1. 没有缓存过期 —— 用户切走再回来,数据是旧的吗?
// 2. 没有去重 —— 两个组件同时 mount,发两次请求
// 3. 没有后台刷新 —— 用户看到的是上次的数据还是刚刚拿的?
// 4. 没有乐观更新 —— mutation 完要等响应才更新 UI
// 5. 代码量巨大 —— 每个资源都要写一遍

// ── 纠正:用 RTK Query 或 TanStack Query ──
// 以上所有问题都由缓存层自动解决
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

7.3 反模式 3:过度使用 Context 导致级联渲染 ​

Context 值变化时,useContext 的所有组件都会重渲染——即使该组件只用了上下文的某个字段,且该字段并未变化。

tsx
// ── 反模式 ──
const AppStateContext = createContext({
  user: null as User | null,
  theme: 'light' as 'light' | 'dark',
  sidebarOpen: false,
})

// 主题切换时,只关心 user 的组件也重渲染了

// ── 纠正方案 A:拆分 Context ──
const UserContext = createContext<User | null>(null)
const ThemeContext = createContext<'light' | 'dark'>('light')
const SidebarContext = createContext(false)

// ── 纠正方案 B:用 Zustand(更推荐) ──
// 因为拆分后的 Context 很容易陷入 Provider 嵌套地狱
const useUserStore = create<UserStore>(/* ... */)
const useThemeStore = create<ThemeStore>(/* ... */)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

7.4 反模式 4:在 Zustand 中订阅整个 Store ​

ts
// ── 反模式 ──
function ExpensiveComponent() {
  const store = useStore()                  // 订阅整个 store
  return <div>{store.bears}</div>           // 但只用了 bears
}
// 任何字段(fish, addBear, ...)变化都会触发重渲染

// ── 纠正 ──
function CheapComponent() {
  const bears = useStore((s) => s.bears)    // 精确订阅
  return <div>{bears}</div>
}
1
2
3
4
5
6
7
8
9
10
11
12

7.5 反模式 5:Store 中放入不可序列化的值 ​

ts
// ── 反模式 ──
interface EditorState {
  domRef: React.RefObject<HTMLDivElement>   // ❌ DOM 节点引用
  websocket: WebSocket                       // ❌ 运行时对象
  uploadPromise: Promise<string>             // ❌ Promise
  formatter: (text: string) => string        // ❌ 函数
}

// 后果:
// • Redux DevTools 时间旅行失效
// • persist 中间件无法序列化
// • SSR hydration 不匹配
// • 很难测试

// ── 纠正:只存可序列化的数据 ──
interface EditorState {
  documentId: string
  content: string
  isConnected: boolean     // websocket 的连接状态
  uploadStatus: 'idle' | 'uploading' | 'done' | 'error'
}
// WebSocket 实例、DOM ref、Promise 留在组件 ref 或外部模块中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

7.6 反模式 6:Next.js 中服务端请求间共享 Store ​

┌─────────────────────────────────────────────────────────────┐
│  Next.js SSR 中的 Store 安全                                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  问题:                                                     │
│  ❌ 导出一个模块级单例 store                                 │
│     export const store = create(...)                        │
│     → 所有请求共享同一个 store 实例                          │
│     → 用户 A 的数据泄漏给用户 B                              │
│                                                             │
│  纠正:                                                     │
│  ✅ 使用 Store 工厂函数                                      │
│     const makeStore = () => configureStore({ reducer })     │
│     每次请求调用 makeStore() 创建独立实例                    │
│                                                             │
│  ✅ 使用 next-redux-wrapper 或手动管理                      │
│     https://github.com/kirill-konshin/next-redux-wrapper    │
│                                                             │
│  ✅ Zustand 同理:                                           │
│     export const createBearStore = () => create(...)        │
│     在 React Server Component 边界不要访问 store            │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ts
// ── 安全的 Next.js Store 工厂 ──
import { configureStore } from '@reduxjs/toolkit'

export const makeStore = () =>
  configureStore({
    reducer: { /* slices */ },
  })

type AppStore = ReturnType<typeof makeStore>
type RootState = ReturnType<AppStore['getState']>
type AppDispatch = AppStore['dispatch']
1
2
3
4
5
6
7
8
9
10
11

核心总结 ​

总结 1:状态生命周期决定方案 ​

局部 UI → useState
可分享导航 → URL
服务端缓存 → TanStack Query / RTK Query
低频全局 → Context
轻量跨组件 → Zustand
复杂业务流程 → Redux Toolkit
1
2
3
4
5
6

总结 2:Redux 数据流 ​

View → dispatch(action) → Store → Reducer(action, oldState) → newState
                                                                    ↓
View ← useSelector 订阅 ← Store ← 不可变新状态存入
1
2
3

总结 3:RTK 三板斧 ​

API用途关键特性
createSlice定义状态 + reducerImmer 允许 "mutating" 语法
createAsyncThunk异步逻辑入 Store自动 pending/fulfilled/rejected
createApi (RTK Query)服务端缓存零手写 reducer,tag-based 失效

总结 4:Zustand 核心 ​

一个 store = 一个 hook
无 Provider,无 action type,无 reducer switch-case
性能靠选择器 + shallow 比较
异步靠 async/await 直接写在 set 里
中间件管线:immer → persist → devtools → subscribeWithSelector
1
2
3
4
5

总结 5:Jotai 定位 ​

原子化模型,每个 atom 独立存在
派生 atom 自动追踪依赖
适合"多个独立的小状态需要按需组合"的场景
与 Zustand 互补而非替代
1
2
3
4

总结 6:选型速查 ​

问题答案
只有一个组件用useState
需要后退/书签URL searchParams
来自服务端TanStack Query / RTK Query
主题/语言/用户Context
中等复杂度、轻量Zustand
多 slice、审计、DevToolsRedux Toolkit
许多独立小状态Jotai

章节测试 ​

测试 1:Redux 数据流 ​

请按顺序写出 Redux 单向数据流中涉及的全部五个环节,并说明每个环节的职责。

测试 2:Immer 原理 ​

Redux Toolkit 的 createSlice 允许在 reducer 中直接 state.count += 1,这是如何实现的?底层做了什么?

测试 3:RTK Query 缓存 ​

RTK Query 中,providesTags 和 invalidatesTags 分别起什么作用?描述一个 POST mutation 创建新数据后自动刷新列表的完整流程。

测试 4:Zustand 选择器 ​

以下代码有什么问题?如何纠正?

ts
function BearCounter() {
  const { bears } = useBearStore()
  return <div>{bears}</div>
}
1
2
3
4

测试 5:Context 边界 ​

列出三种不适合用 React Context 管理状态的场景,并说明原因和替代方案。

测试 6:状态选型 ​

一个应用中同时存在以下 6 种状态,请为每种选择最合适的方案并说明理由:

  1. 当前选中的 Tab 页签
  2. 用户搜索关键词和筛选条件
  3. 商品列表(来自 API)
  4. 当前登录用户信息
  5. 多步骤结账流程的状态
  6. 文件上传进度

测试 7:Next.js Store 安全 ​

为什么在 Next.js 中不能直接导出模块级单例的 Zustand store 或 Redux store?应该怎么做?


参考答案 ​

测试 1 答案 ​

答案:Action Creator → Dispatch → Store → Reducer → Selector。

  • Action Creator:创建描述"发生了什么"的 action 对象
  • Dispatch:将 action 发送给 Store
  • Store:持有当前 state,将 action 和 state 转发给 Reducer
  • Reducer:纯函数 (state, action) => newState,根据 action 计算新状态
  • Selector:从 Store 中提取组件需要的数据,决定是否重渲染

测试 2 答案 ​

答案:Redux Toolkit 内部集成了 Immer 库。Immer 通过 ES6 Proxy 拦截对 state 的修改操作,将这些"可变"操作记录下来,最后基于原始 state 生成一个不可变的 新对象。所以开发者写的是 state.count += 1,但 Immer 实际生成的是 { ...state, count: state.count + 1 }。这使得 reducer 代码简洁但底层仍满足 Redux 的不可变要求。


测试 3 答案 ​

答案:

  • providesTags:query endpoint 声明"我提供这些标签的数据"。当标签被 invalidated 时,该 query 会重新获取。
  • invalidatesTags:mutation endpoint 声明"我完成后使这些标签的数据失效"。

完整流程:

  1. useCreatePostMutation 调用 POST 请求创建新文章
  2. 请求成功后,invalidatesTags: ['Post'] 将 'Post' 标签标记为失效
  3. 所有 providesTags 中包含 'Post' 的 query(如 useGetPostsQuery)自动重新获取
  4. 列表组件自动显示包含新文章的最新数据

测试 4 答案 ​

答案:const { bears } = useBearStore() 没有传选择器,订阅了整个 store。这意味着 store 中 任何 字段(fish、addBear 等)变化都会导致 BearCounter 重渲染。

纠正:使用精确选择器 — const bears = useBearStore((s) => s.bears)。这样只有 bears 字段变化时才重渲染。


测试 5 答案 ​

答案:

  1. 高频更新的状态(如鼠标位置、动画帧):Context 值变化会导致所有消费者重渲染,高频更新会造成严重性能问题。替代方案:Zustand。
  2. 被深层子树中少数组件消费的大型状态:所有消费者都在同一个 Context 下,任何字段变化都触发全局重渲染。替代方案:Zustand + 选择器,或 Jotai 原子化。
  3. 需要选择器订阅细粒度更新的复杂状态:Context 没有内置选择器机制,开发者需要手动用 useMemo 优化,容易出错。替代方案:Zustand / Redux Toolkit。

测试 6 答案 ​

答案:

  1. Tab 页签 → useState。只在一个组件或小组件内使用,不需要共享。
  2. 搜索/筛选 → URL searchParams。可分享、可书签、支持前进后退。
  3. 商品列表 → TanStack Query / RTK Query。服务端数据,需要缓存、去重、过期管理。
  4. 用户信息 → Context。低频更新、全局消费、值几乎不变。
  5. 结账流程 → Zustand。跨多个步骤/组件,中等复杂度,无需 Redux 的强约束。
  6. 上传进度 → useState(在表单组件中)或 Zustand(如需跨组件展示进度条)。

测试 7 答案 ​

答案:Node.js 服务端是长驻进程,多个请求共享同一个模块作用域。如果导出模块级单例 store,所有请求都会读写同一个 store 实例,导致用户 A 的数据泄漏给用户 B。这是严重的跨请求状态污染。

纠正:使用 Store 工厂模式——导出一个工厂函数(如 makeStore()),在每次请求/每次渲染时创建独立的 store 实例。Next.js 的 next-redux-wrapper 或 Zustand 的 createStore 工厂模式都能正确实现请求级隔离。


相关笔记 ​

  • [[02-hooks-state-and-effects]] - React Hooks、useState、useEffect、useReducer
  • [[01-nextjs-app-router]] - Next.js App Router 架构与服务端渲染
  • TanStack Query 官方文档:https://tanstack.com/query/latest
  • Redux 风格指南:https://redux.js.org/style-guide
  • Zustand 官方文档:https://zustand.docs.pmnd.rs/

下一步学习 ​

  • [ ] 阅读 [[01-function-calling]] — Agent 基础(如果学习路线涉及 AI)
  • [ ] 阅读 TanStack Query 官方文档,完成至少一个项目实战
  • [ ] 将现有项目中手写的 loading/error reducer 迁移到 RTK Query 或 TanStack Query
  • [ ] 实践 Zustand 的 persist + devtools 组合,完成一个带持久化的主题切换功能
  • [ ] 对比 Zustand 和 Jotai 在实际项目中的使用体验,形成自己的选型直觉

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇4. 路由、表单与组件架构 / Routing, Forms, and Component Architecture
下一篇6. Next.js App Router:路由、渲染与工程边界 / Next.js App Router, Routing, Rendering, and Engineering Boundaries

持续记录,持续成长

Copyright © Tidenflow