Zustand状态管理模式在React Native中的应用
问题陈述
Zustand的简单性隐藏了重要的时间细节。set()是同步的,但React的重新渲染是批量的。getState()逃避了过时的闭包。在商店中的异步操作需要小心处理。理解这些内部机制可以防止微妙的错误。
模式:set()是同步的,渲染是批量的
**问题:**假设在set()之后状态立即为React“准备就绪”。
const useStore = create((set, get) => ({
count: 0,
increment: () => {
set({ count: get().count + 1 });
// 状态在这里已经更新(set是同步的)
console.log(get().count); // ✅ 显示新值
// 但React还没有重新渲染
// 组件将在下一个渲染周期看到旧值
},
}));
关键洞察:
set()同步更新商店getState()立即反映新值- React组件异步重新渲染(批量)
何时重要:
- 链式多个状态更新
- 更新后验证状态
- 调试“过时”的组件值
模式:getState()逃避过时的闭包
**问题:**回调和异步函数在创建时捕获状态。使用get()或getState()始终获得当前状态。
const useStore = create((set, get) => ({
answers: {},
// 错误 - 状态在函数创建时被捕获
saveAnswerBad: (questionId: string, value: number) => {
setTimeout(() => {
const answers = get().answers; // ❌ 这是可以的
// 但如果有人将`answers`作为参数传递...
}, 1000);
},
// 正确 - 总是使用get()获取当前状态
saveAnswer: async (questionId: string, value: number) => {
await someAsyncOperation();
// 在await之后,使用get()确保当前状态
const currentAnswers = get().answers;
set({ answers: { ...currentAnswers, [questionId]: value } });
},
}));
// 在组件中 - 同样的原则
function Component() {
const answers = useStore((s) => s.answers);
const handleSave = async () => {
await delay(1000);
// 这里的answers是过时的!在渲染时被捕获
// 使用getState()获取当前值
const current = useStore.getState().answers;
};
}
**规则:**在任何await之后,使用get()或getState() - 永远不要依赖闭包捕获的值。
模式:商店中的异步操作
**问题:**异步操作需要明确的async/await和在awaits之后仔细读取状态。
const useStore = create((set, get) => ({
loading: false,
data: null,
error: null,
// 错误 - 没有async关键字,容易产生竞态条件
fetchDataBad: (id: string) => {
set({ loading: true });
api.fetch(id).then((data) => {
set({ data, loading: false });
});
// 立即返回,调用者无法等待
},
// 正确 - 适当的异步操作
fetchData: async (id: string) => {
set({ loading: true, error: null });
try {
const data = await api.fetch(id);
// 在await之后如果需要,重新读取状态
if (get().loading) { // 检查我们是否仍在加载状态
set({ data, loading: false });
}
} catch (error) {
set({ error: error.message, loading: false });
}
},
}));
// 调用者可以正确等待
await useStore.getState().fetchData('123');
模式:选择器稳定性
**问题:**选择器创建新对象导致不必要的重新渲染。
// 错误 - 每次渲染都创建新对象
const data = useStore((state) => ({
name: state.name,
count: state.count,
}));
// 正确 - 使用多个选择器
const name = useStore((state) => state.name);
const count = useStore((state) => state.count);
// 或者 - 使用浅比较(Zustand 4.x)
import { shallow } from 'zustand/shallow';
const { name, count } = useStore(
(state) => ({ name: state.name, count: state.count }),
shallow
);
// Zustand 5.x - 使用useShallow钩子
import { useShallow } from 'zustand/react/shallow';
const { name, count } = useStore(
useShallow((state) => ({ name: state.name, count: state.count }))
);
模式:派生状态
**问题:**在选择器中计算派生值与存储它们。
const useStore = create((set, get) => ({
answers: {},
// 错误 - 存储可能变得过时的派生状态
totalAnswers: 0,
updateTotalAnswers: () => {
set({ totalAnswers: Object.keys(get().answers).length });
},
// 正确 - 在选择器中计算(始终新鲜)
// answers: {}, // 只存储源数据
}));
// 选择器计算派生值
const totalAnswers = useStore((state) => Object.keys(state.answers).length);
// 对于昂贵的计算,在外面记忆
import { useMemo } from 'react';
function Component() {
const answers = useStore((state) => state.answers);
const expensiveResult = useMemo(() => {
return computeExpensiveAnalysis(answers);
}, [answers]);
}
模式:商店订阅用于副作用
**问题:**需要在React组件之外响应状态变化。
// 订阅特定状态变化
const unsubscribe = useStore.subscribe(
(state) => state.answers,
(answers, prevAnswers) => {
console.log('Answers changed:', { prev: prevAnswers, current: answers });
// 持久化到存储,发送分析等。
},
{ equalityFn: shallow }
);
// 在Zustand 4.x中使用subscribeWithSelector中间件
import { subscribeWithSelector } from 'zustand/middleware';
const useStore = create(
subscribeWithSelector((set, get) => ({
answers: {},
// ...
}))
);
模式:测试Zustand商店
**问题:**测试需要重置商店状态并验证异步流程。
// 带有重置功能的商店
const initialState = {
answers: {},
loading: false,
};
const useStore = create((set, get) => ({
...initialState,
// 动作...
// 重置用于测试
_reset: () => set(initialState),
}));
// 测试
describe('Assessment Store', () => {
beforeEach(() => {
useStore.getState()._reset();
});
it('saves answers during retake flow', async () => {
const store = useStore.getState();
// 完整的异步流程
await store.loadCompletedAnswers(assessmentId);
await store.enableSkillAreaRetake('fundamentals');
// 在异步之后验证状态
expect(store.getState().retakeAreas).toContain('fundamentals');
// 继续流程
await store.saveAnswer('q1', 4);
// 验证最终状态
expect(useStore.getState().userAnswers['q1']).toBe(4);
});
});
模式:调试状态变化
**问题:**追踪状态何时何地意外变化。
// 添加日志记录中间件
import { devtools } from 'zustand/middleware';
const useStore = create(
devtools(
(set, get) => ({
// ...你的商店
}),
{ name: 'AssessmentStore' }
)
);
// 特定调试的手动日志记录
const useStore = create((set, get) => ({
answers: {},
saveAnswer: (questionId: string, value: number) => {
console.log('[saveAnswer] Before:', {
questionId,
value,
currentAnswers: get().answers,
retakeAreas: get().retakeAreas,
});
set((state) => ({
answers: { ...state.answers, [questionId]: value },
}));
console.log('[saveAnswer] After:', {
answers: get().answers,
});
},
}));
常见陷阱
| 陷阱 | 解决方案 |
|---|---|
| 在await之后过时的闭包 | 在每个await之后使用get() |
| 选择器返回新对象 | 使用shallow或多个选择器 |
| 动作不可等待 | 添加async关键字,返回承诺 |
| 组件中的状态似乎过时 | 组件还没有重新渲染 - 使用getState()进行即时读取 |
| 无法找到状态何时变化 | 添加devtools中间件或手动日志记录 |
Zustand 5.x迁移说明
如果从4.x升级:
// 4.x - shallow来自主包
import { shallow } from 'zustand/shallow';
// 5.x - useShallow钩子用于React
import { useShallow } from 'zustand/react/shallow';
// 4.x - 通常需要类型参数
const useStore = create<StoreType>()((set, get) => ({...}));
// 5.x - 改进的类型推断
const useStore = create((set, get) => ({...}));