Zustand vs Jotai:为你的 React 应用选择合适的状态管理器
从性能、TypeScript 支持与心智模型三个维度对比 Zustand 和 Jotai,帮助为 React 项目选择合适的状态管理库。
刚接触 Zustand 或 Jotai?请先查看我们的深度指南:
React 状态管理已经显著发展,超越了 Redux 的复杂性。对于中小型项目,像 Zustand 和 Jotai 这样的轻量级替代方案已经获得了广泛欢迎。但你应该选择哪一个呢?本文比较了这两个由同一开发者(Daishi Kato)创建的库,帮助你根据项目需求做出明智的决定。
核心要点
- Zustand 使用集中式、自上而下的方法,适合相互关联的状态和团队协作
- Jotai 使用原子化、自下而上的方法,非常适合细粒度响应式和快速变化的数据
- 两个库都是轻量级、高性能且对 TypeScript 友好
- Zustand 通常更适合具有复杂状态关系的大型应用程序
- Jotai 在需要独立状态片段且最小化重新渲染的场景中表现出色
Zustand 和 Jotai 的起源和理念
这两个库都是为了解决 React 生态系统中的特定问题而创建的,但采用了不同的方法:
Zustand:自上而下的方法
Zustand(德语”状态”的意思)于 2019 年发布,作为 Redux 的更简单替代方案。它遵循集中式、自上而下的状态管理方法。
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
Jotai:自下而上的方法
Jotai(日语”状态”的意思)于 2020 年发布,从 Recoil 中获得灵感。它使用原子化、自下而上的方法,将状态分解为小的、独立的原子。
import { atom, useAtom } from 'jotai'
const countAtom = atom(0)
const doubleCountAtom = atom((get) => get(countAtom) * 2)
心智模型:它们如何以不同方式处理状态
理解每个库背后的心智模型对于为你的项目选择正确的库至关重要。
Zustand 的基于 Store 的模型
Zustand 使用包含所有状态和操作的单一 store。这种模型对使用过 Redux 的开发者来说很熟悉:
// 创建一个 store
const useUserStore = create((set) => ({
user: null,
isLoading: false,
error: null,
fetchUser: async (id) => {
set({ isLoading: true });
try {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
set({ user, isLoading: false });
} catch (error) {
set({ error, isLoading: false });
}
}
}));
// 在组件中使用 store
function Profile({ userId }) {
const { user, fetchUser } = useUserStore(
state => ({ user: state.user, fetchUser: state.fetchUser })
);
useEffect(() => {
fetchUser(userId);
}, [userId, fetchUser]);
return user ? <div>{user.name}</div> : <div>Loading...</div>;
}
Jotai 的原子模型
Jotai 将状态分解为可以组合在一起的原子。这类似于 React 自己的 useState,但具有跨组件共享状态的能力:
// 创建原子
const userAtom = atom(null);
const isLoadingAtom = atom(false);
const errorAtom = atom(null);
// 创建派生原子
const userStatusAtom = atom(
(get) => ({
user: get(userAtom),
isLoading: get(isLoadingAtom),
error: get(errorAtom)
})
);
// 创建操作原子
const fetchUserAtom = atom(
null,
async (get, set, userId) => {
set(isLoadingAtom, true);
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
set(userAtom, user);
set(isLoadingAtom, false);
} catch (error) {
set(errorAtom, error);
set(isLoadingAtom, false);
}
}
);
// 在组件中使用原子
function Profile({ userId }) {
const [{ user, isLoading }] = useAtom(userStatusAtom);
const [, fetchUser] = useAtom(fetchUserAtom);
useEffect(() => {
fetchUser(userId);
}, [userId, fetchUser]);
return user ? <div>{user.name}</div> : <div>Loading...</div>;
}
性能考虑:Zustand vs Jotai
两个库都被设计为高性能的,但它们针对不同的场景进行优化。
Zustand 的性能特征
- 选择性订阅:组件仅在其选择的状态发生变化时重新渲染
- 包大小:压缩后约 2.8kB
- 中间件支持:内置中间件用于性能优化
- 批量更新:自动批量处理状态更新
Jotai 的性能特征
- 细粒度更新:只有使用特定原子的组件才会重新渲染
- 包大小:压缩后约 3.5kB(核心包)
- 原子级优化:对哪些状态变化触发重新渲染进行细粒度控制
- 派生状态:高效处理计算值
对于快速变化且仅影响 UI 特定部分的数据,Jotai 的原子方法通常导致更少的重新渲染。对于变化频率较低的相互关联状态,Zustand 的方法可能更高效。
TypeScript 集成
两个库都提供出色的 TypeScript 支持,但采用不同的方法。
Zustand 与 TypeScript
interface BearState {
bears: number;
increase: (by: number) => void;
}
const useBearStore = create<BearState>((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
}));
Jotai 与 TypeScript
interface User {
id: string;
name: string;
}
const userAtom = atom<User | null>(null);
const nameAtom = atom(
(get) => get(userAtom)?.name || '',
(get, set, newName: string) => {
const user = get(userAtom);
if (user) {
set(userAtom, { ...user, name: newName });
}
}
);
何时选择 Zustand
在以下情况下,Zustand 通常是更好的选择:
-
你需要集中式 store:对于具有需要从许多组件访问和修改的相互关联状态的应用程序。
-
你正在从 Redux 迁移:Zustand 的 API 对 Redux 用户来说更熟悉,使迁移更容易。
-
你需要非 React 访问状态:Zustand 允许你在 React 组件外部访问和修改状态。
-
团队协作是优先考虑的:集中式 store 方法在大型团队中可能更容易维护。
-
你更喜欢显式状态更新:Zustand 的方法使状态变化更容易跟踪。
何时选择 Jotai
Jotai 在以下情况下表现出色:
-
你需要细粒度响应式:对于具有许多独立状态片段且频繁变化的 UI。
-
你正在构建复杂表单:Jotai 的原子方法适用于需要独立验证的表单字段。
-
你想要类似 useState 的 API:如果你更喜欢与 React 内置 hooks 非常相似的 API。
-
你正在处理快速变化的数据:对于最小化重新渲染至关重要的实时应用程序。
-
你需要派生状态:Jotai 使基于其他状态创建计算值变得容易。
实际实现模式
让我们看看在两个库中实现的一些常见模式。
身份验证状态
使用 Zustand:
const useAuthStore = create((set) => ({
user: null,
isAuthenticated: false,
isLoading: false,
login: async (credentials) => {
set({ isLoading: true });
try {
const user = await loginApi(credentials);
set({ user, isAuthenticated: true, isLoading: false });
} catch (error) {
set({ isLoading: false });
throw error;
}
},
logout: async () => {
await logoutApi();
set({ user: null, isAuthenticated: false });
}
}));
使用 Jotai:
const userAtom = atom(null);
const isAuthenticatedAtom = atom((get) => !!get(userAtom));
const isLoadingAtom = atom(false);
const loginAtom = atom(
null,
async (get, set, credentials) => {
set(isLoadingAtom, true);
try {
const user = await loginApi(credentials);
set(userAtom, user);
set(isLoadingAtom, false);
} catch (error) {
set(isLoadingAtom, false);
throw error;
}
}
);
const logoutAtom = atom(
null,
async (get, set) => {
await logoutApi();
set(userAtom, null);
}
);
表单状态管理
使用 Zustand:
const useFormStore = create((set) => ({
values: { name: '', email: '', message: '' },
errors: {},
setField: (field, value) => set(state => ({
values: { ...state.values, [field]: value }
})),
validate: () => {
// 验证逻辑
const errors = {};
set({ errors });
return Object.keys(errors).length === 0;
},
submit: () => {
// 提交逻辑
}
}));
使用 Jotai:
const formAtom = atom({ name: '', email: '', message: '' });
const nameAtom = atom(
(get) => get(formAtom).name,
(get, set, name) => set(formAtom, { ...get(formAtom), name })
);
const emailAtom = atom(
(get) => get(formAtom).email,
(get, set, email) => set(formAtom, { ...get(formAtom), email })
);
const messageAtom = atom(
(get) => get(formAtom).message,
(get, set, message) => set(formAtom, { ...get(formAtom), message })
);
const errorsAtom = atom({});
const validateAtom = atom(
null,
(get, set) => {
// 验证逻辑
const errors = {};
set(errorsAtom, errors);
return Object.keys(errors).length === 0;
}
);
迁移策略
从 Redux 到 Zustand
// Redux
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => {
state.value += 1;
},
decrement: state => {
state.value -= 1;
}
}
});
// Zustand
const useCounterStore = create((set) => ({
value: 0,
increment: () => set(state => ({ value: state.value + 1 })),
decrement: () => set(state => ({ value: state.value - 1 }))
}));
从 Context API 到 Jotai
// Context API
const CounterContext = createContext();
function CounterProvider({ children }) {
const [count, setCount] = useState(0);
return (
<CounterContext.Provider value={{ count, setCount }}>
{children}
</CounterContext.Provider>
);
}
// Jotai
const countAtom = atom(0);
function App() {
return <>{/* 不需要 provider */}</>;
}
常见陷阱和最佳实践
Zustand 陷阱
- Store 碎片化:创建太多 store 可能导致状态管理混乱。
- 选择器记忆化:忘记记忆化选择器可能导致不必要的重新渲染。
- 中间件过度使用:添加太多中间件可能影响性能。
Jotai 陷阱
- 原子激增:创建太多原子而没有组织可能使代码难以理解。
- 循环依赖:创建相互循环依赖的原子。
- 原子重复:意外创建同一原子的多个实例。
两者的最佳实践
- 组织相关状态:将相关状态和操作组合在一起。
- 使用 TypeScript:两个库都受益于 TypeScript 的类型安全。
- 记录你的状态结构:明确你的状态是如何组织的。
- 测试你的状态逻辑:为你的状态管理代码编写单元测试。
结论
在 Zustand 和 Jotai 之间选择取决于你的具体项目需求。Zustand 提供了一种集中式方法,适用于大型应用程序中复杂、相互关联的状态。Jotai 提供了一种原子模型,在细粒度响应式和最小重新渲染方面表现出色。两个库都提供轻量级、高性能的解决方案,显著改善了 Redux 的复杂性,同时保持 TypeScript 兼容性。
在做决定时,请考虑你的团队对不同状态管理模式的熟悉程度、应用程序的性能需求和状态结构。记住,你甚至可以在同一个应用程序中使用两个库,利用每个库的优势。
常见问题
我可以同时使用 Zustand 和 Jotai 吗?
是的,许多开发者使用 Zustand 管理全局应用状态,使用 Jotai 管理需要在组件树中共享的组件特定状态。
这些库适合大规模应用程序吗?
是的,如果使用得当,两者都可以扩展到大型应用程序。由于其集中式方法,Zustand 在大型团队设置中可能更容易维护。
这些库与 Redux 相比如何?
两者都比 Redux 显著更轻量和简单。Zustand 在理念上更接近 Redux,但样板代码要少得多。Jotai 采用完全不同的方法,专注于原子状态。
我需要用 provider 包装我的应用吗?
Zustand 默认不需要 provider。Jotai 可以在不使用 provider 的情况下用于全局原子,但为作用域状态提供了 provider。
我可以在 Next.js 或其他框架中使用这些库吗?
是的,两个库都与 Next.js 和其他 React 框架配合良好。它们提供了特定的实用程序来支持服务器端渲染。