09 — State 与 useState

这一章讲 React 另一半核心:State — 组件内部会变化的数据。
学完这章你就理解了 React 自动更新 UI 的机制。


1. 问题引入:为什么需要 State?

1.1 普通变量不行吗?

1
2
3
4
5
6
7
8
9
10
function Counter() {
let count = 0; // 普通变量

return (
<div>
<p>{count}</p>
<button onClick={() => count++}>+1</button>
</div>
);
}

问题:点按钮,count 确实变了,但页面不更新

原因:React 不知道你改了 count,所以不会重新渲染。

1.2 解决方案:useState

1
2
3
4
5
6
7
8
9
10
11
12
13
import { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);
// ↑ 数组解构:count 是当前值,setCount 是改它的函数

return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}

现在点按钮:

  1. 调用 setCount(count + 1)
  2. React 检测到 state 变化
  3. 自动重新调用 Counter() 函数
  4. 新的 JSX 包含新 count
  5. DOM 自动更新

2. useState 详解

2.1 语法

1
2
3
const [value, setValue] = useState(initialValue);
// ↑ ↑ ↑
// 当前值 修改函数 初始值
  • 初始值:只在第一次渲染时生效
  • value:当前 state 值
  • setValue:调用它会触发重新渲染

2.2 例子:各种类型的 State

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 数字
const [count, setCount] = useState(0);

// 字符串
const [name, setName] = useState('豆豆');

// 布尔
const [active, setActive] = useState(false);

// 数组
const [list, setList] = useState([]);

// 对象
const [user, setUser] = useState({ name: 'A', age: 30 });

// 复杂对象
const [chat, setChat] = useState({
id: '1',
title: '新对话',
messages: [],
});

// 联合类型(TypeScript)
const [status, setStatus] = useState<'idle' | 'loading' | 'done'>('idle');

2.3 初始值可以是一个函数(懒初始化)

1
2
3
4
5
6
7
8
// 简单的初始值直接传
const [count, setCount] = useState(0);

// 复杂计算用函数形式(避免每次渲染都重新计算)
const [data, setData] = useState(() => {
const initial = computeExpensiveValue();
return initial;
});

项目里例子

1
2
3
4
5
6
7
8
9
10
// Sidebar.tsx
const [collapsed, setCollapsed] = useState<boolean>(() => {
if (typeof window === 'undefined') return false;
try {
return localStorage.getItem(STORAGE_KEY) === 'true';
} catch {
return false;
}
});
// ↑ 因为从 localStorage 读要 try/catch,所以用函数形式

3. 修改 State 的正确姿势

3.1 基本更新

1
2
3
4
5
const [count, setCount] = useState(0);

setCount(5); // 直接设值
setCount(count + 1); // 基于当前值
setCount(prev => prev + 1); // 函数式更新(推荐)

为什么用函数式更新?

1
2
3
4
5
6
7
8
9
10
11
12
// 三次连续 +1
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// 最终 count = 1(不是 3)

// 因为 setCount 后 count 不会立即变,三次都读到同一个 count
// 函数式更新保证基于最新值
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// 最终 count = 3 ✓

3.2 修改对象 State

重要原则:永远创建新对象,不要直接修改

1
2
3
4
5
6
7
8
9
const [user, setUser] = useState({ name: 'A', age: 30 });

// ❌ 错的:直接修改
user.age = 31;
setUser(user); // React 检测不到变化(对象引用没变)

// ✅ 对的:创建新对象
setUser({ ...user, age: 31 }); // 浅拷贝 + 覆盖
setUser(prev => ({ ...prev, age: 31 })); // 函数式更安全

3.3 修改数组 State

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const [list, setList] = useState([1, 2, 3]);

// 末尾追加
setList([...list, 4]);

// 头部追加
setList([0, ...list]);

// 删除某项
setList(list.filter(item => item !== 2));

// 替换某项
setList(list.map(item => item === 2 ? 20 : item));

// ❌ 错的:直接修改
list.push(4);
setList(list);

3.4 项目里的真实例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// EmptyState.tsx(项目代码简化)
function EmptyState({ onQuickPrompt }: Props) {
return (
<div>
{QUICK_ACTIONS.map((a) => (
<button
key={a.text}
onClick={() => onQuickPrompt(a.text)}
>
{a.text}
</button>
))}
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// store/chatStore.ts — 修改 State 的正确姿势
async sendMessage() {
// ... 准备消息 ...

// 1. 创建用户消息对象
const userMsg: ChatMessage = {
id: generateId('msg'),
role: 'user',
content: message,
file: hasFile,
fileName: fileToSend?.name ?? null,
timestamp: Date.now(),
};

// 2. 通过 setState 整体更新
set({
chatList: [...get().chatList], // 触发 React 更新
inputMessage: '',
isSending: true,
});
}

4. State 在哪里?

4.1 组件 State — useState

  • 每个组件各自持有自己的 State
  • 组件间不共享(除非用其他机制)
1
2
3
4
5
6
7
8
9
10
11
function CounterA() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>A: {count}</button>;
}

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

// A 和 B 互不影响

4.2 全局 State — Zustand

项目里绝大多数 State 都在 Store 里@/store/chatStore.ts)。

为什么?

  • 多个组件需要共享数据(聊天列表、当前会话 ID)
  • 跨组件通信太繁琐(层层传 Props)

Store 的本质:一个全局对象,组件按需订阅。

1
2
3
4
// 任何组件都可以用:
const chatList = useChatStore((s) => s.chatList);
const currentChatId = useChatStore((s) => s.currentChatId);
const isSending = useChatStore((s) => s.isSending);

详细看 14-Zustand状态管理.md

4.3 项目里 useState 用得不多

1
2
# 搜索项目里所有 useState
grep -r "useState" src/ --include="*.tsx"

项目里只在以下地方用

  • Sidebar.tsx — 折叠状态 collapsed
  • Sidebar.tsx — 媒体查询 useMediaQuery(自定义 Hook 内部用)
  • InputArea.tsxuseTextareaAutosize — textarea ref

其他所有动态数据(聊天列表、消息、输入框文字、Agent 选择等)都在 Zustand Store


5. State 变化触发的连锁反应

5.1 完整流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
用户操作(点按钮、输入文字)

调用 setState / store action

React 收到通知

React 重新调用函数组件

得到新的 JSX(虚拟 DOM)

React 对比新旧 JSX(diff 算法)

更新真实 DOM(只更新变化的部分)

浏览器渲染

5.2 重新渲染的范围

1
2
3
4
5
6
7
8
9
10
function App() {
const [count, setCount] = useState(0);
return (
<div>
<Header />
<Counter count={count} />
<Footer />
</div>
);
}

setCount(1) 触发 App 重新渲染。
React 会重新调用 AppHeaderCounterFooter
只更新实际变化的 DOM(这里 Counter 的数字变了,其他不变)。

性能优化:用 React.memouseMemo 减少不必要的重新渲染(项目里没用,因为数据量小)。


6. 受控组件 — 项目的 input/textarea

6.1 什么是受控组件

组件的值由 React State 控制(而不是 DOM 自己管)。

1
2
3
4
5
6
7
8
9
10
function MyInput() {
const [value, setValue] = useState('');

return (
<input
value={value} // ← value state 控制
onChange={(e) => setValue(e.target.value)} // ← 变化时更新 state
/>
);
}

6.2 项目里 InputArea.tsx(简化)

1
2
3
4
5
6
7
8
9
10
11
12
// InputArea.tsx
function InputArea() {
const inputMessage = useChatStore(s => s.inputMessage); // 从 Store 读
const setInput = useChatStore(s => s.setInput);

return (
<textarea
value={inputMessage} // ← 受控值来自 Store
onChange={(e) => setInput(e.target.value)} // ← 变化时更新 Store
/>
);
}

为什么用 Store 而不是 local state?

  • 其他组件可能需要读取输入内容(比如快捷问题)
  • 全局共享一份,避免数据不一致

6.3 非受控组件(项目里没有但要知道)

1
2
3
4
5
6
// 用 ref 拿到 DOM 元素,DOM 自己管值
function MyInput() {
const ref = useRef(null);
return <input ref={ref} defaultValue="初始值" />;
// ↑ defaultValue 只在初始生效
}

项目里只用受控组件,逻辑更清晰。


7. 实战:项目里所有 useState 的位置和作用

1
2
3
4
5
6
7
8
9
10
11
12
// hooks/useAutoScroll.ts
const ref = useRef<HTMLDivElement>(null);
// ↑ 用 ref 不用 useState(因为不需要触发渲染)

// Sidebar.tsx
const [collapsed, setCollapsed] = useState<boolean>(() => {
// 从 localStorage 读初始值
return localStorage.getItem(STORAGE_KEY) === 'true';
});

const [matches, setMatches] = useState<boolean>(...);
// ↑ 在自定义 Hook useMediaQuery 内部

仅此而已。其他所有「会变的数据」都在 Zustand。


8. 常见错误

错误 1:忘记 useState,直接修改

1
2
3
4
5
6
7
// ❌ 错的
let count = 0;
count++; // 不会触发重新渲染

// ✅ 对的
const [count, setCount] = useState(0);
setCount(count + 1);

错误 2:直接修改 State 对象

1
2
3
4
5
6
7
8
// ❌ 错的
const [user, setUser] = useState({ name: 'A' });
user.name = 'B';
setUser(user); // React 不知道对象变了

// ✅ 对的
setUser({ ...user, name: 'B' });
setUser(u => ({ ...u, name: 'B' }));

错误 3:连续 setState 期望合并

1
2
3
4
5
6
7
8
9
10
// ❌ 错的:期望 count 变 3
const [count, setCount] = useState(0);
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);

// ✅ 对的
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);

错误 4:把 useState 写到 if/for 里(违反 Hooks 规则)

1
2
3
4
5
6
7
8
9
10
// ❌ 错的
if (someCondition) {
const [count, setCount] = useState(0);
}

// ✅ 对的:所有 Hook 必须在组件顶层调用
const [count, setCount] = useState(0);
if (someCondition) {
// 只在 setCount 时判断
}

9. 一段话总结

State = 组件内部会变化、变化会触发重新渲染的数据。
useState(initial) 返回 [value, setValue],调用 setValue 触发重新渲染。
修改原则:对象/数组要创建新对象(不可变更新)。
项目里:极少用 useState(只有折叠状态等),其他数据都在 Zustand Store。


接下来

State 懂了。下一章讲 React 的另一半 — 怎么和「外部世界」交互(API、DOM、定时器):10-副作用useEffect与useRef.md