派生状态模式
多个状态变量从同一个来源推导,而不是各自独立维护,避免多状态错位和手动同步。
#type / concept
#status / growing
#tech / dev / frontend
#resource / react
[!info] related notes
- 前置: useState
- 关联: useMemo, useCallback
- 架构: URL 作为唯一身份源
- 实践: TanStack Query 服务端状态
派生状态模式
一句话定义
如果一个状态可以从另一个状态计算出来,它就不应该被单独 useState 维护,而应该作为计算结果直接派生。
核心机制
问题:多个状态需要手动同步
const [currentConversation, setCurrentConversation] = useState(null);
const [chatSessionKey, setChatSessionKey] = useState(() => crypto.randomUUID());
// 新建会话后
setCurrentConversation(newConv);
setChatSessionKey(`conversation:${newConv.id}`);
navigate(`/consultation/${newConv.id}`);
// 删除会话后
setCurrentConversation(null);
setChatSessionKey('new');
navigate('/consultation');
三个地方要同步改,漏一个就错位。
解决:从单一来源派生
const routeConversationId = id && id !== 'new' ? id : null;
// chatSessionKey 从 routeConversationId 派生
const chatSessionKey = routeConversationId
? `conversation:${routeConversationId}`
: 'new';
// currentConversation 从 query data 派生
const currentConversation = conversationQuery.data?.conversation ?? null;
// messages 从 query data 派生
const messages = conversationQuery.data?.messages ?? [];
现在只需要改一个地方(navigate),其他都自动推导。
最小例子
// ❌ 手动同步
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
// 每次改 firstName 或 lastName 都要 setFullName
// ✅ 派生
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`;
// fullName 自动跟随变化
适用场景
| 场景 | 派生方式 |
|---|---|
| 当前会话 ID | id && id !== 'new' ? id : null |
| 聊天 session key | routeConversationId ? \conversation:${routeConversationId}` : ‘new’` |
| 当前会话详情 | conversationQuery.data?.conversation ?? null |
| 消息列表 | conversationQuery.data?.messages ?? [] |
| 提取信息 | consultationQuery.data?.extracted_info ?? [] |
| 诊断阶段 | consultationQuery.data?.phase ?? 'collecting' |
| 是否加载中 | conversationQuery.isPending |
| 是否有错误 | conversationQuery.isError |
边界与易混淆点
- 派生状态不需要
useEffect来同步,它是同步计算的 - 如果派生计算很重,用
useMemo包裹 - 派生状态和”计算属性”是同一个思路,只是在 React 里需要显式写出来
- 不是所有状态都该派生:频繁变化的输入 + 昂贵的计算 = 考虑
useMemo;简单赋值 = 直接写