Zustand vs 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 在大型团队设置中可能更容易维护。
两者都比 Redux 显著更轻量和简单。Zustand 在理念上更接近 Redux,但样板代码要少得多。Jotai 采用完全不同的方法,专注于原子状态。
Zustand 默认不需要 provider。Jotai 可以在不使用 provider 的情况下用于全局原子,但为作用域状态提供了 provider。
是的,两个库都与 Next.js 和其他 React 框架配合良好。它们提供了特定的实用程序来支持服务器端渲染。