Back

Zustand vs Jotai:为你的 React 应用选择合适的状态管理器

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 通常是更好的选择:

  1. 你需要集中式 store:对于具有需要从许多组件访问和修改的相互关联状态的应用程序。

  2. 你正在从 Redux 迁移:Zustand 的 API 对 Redux 用户来说更熟悉,使迁移更容易。

  3. 你需要非 React 访问状态:Zustand 允许你在 React 组件外部访问和修改状态。

  4. 团队协作是优先考虑的:集中式 store 方法在大型团队中可能更容易维护。

  5. 你更喜欢显式状态更新:Zustand 的方法使状态变化更容易跟踪。

何时选择 Jotai

Jotai 在以下情况下表现出色:

  1. 你需要细粒度响应式:对于具有许多独立状态片段且频繁变化的 UI。

  2. 你正在构建复杂表单:Jotai 的原子方法适用于需要独立验证的表单字段。

  3. 你想要类似 useState 的 API:如果你更喜欢与 React 内置 hooks 非常相似的 API。

  4. 你正在处理快速变化的数据:对于最小化重新渲染至关重要的实时应用程序。

  5. 你需要派生状态: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 陷阱

  1. Store 碎片化:创建太多 store 可能导致状态管理混乱。
  2. 选择器记忆化:忘记记忆化选择器可能导致不必要的重新渲染。
  3. 中间件过度使用:添加太多中间件可能影响性能。

Jotai 陷阱

  1. 原子激增:创建太多原子而没有组织可能使代码难以理解。
  2. 循环依赖:创建相互循环依赖的原子。
  3. 原子重复:意外创建同一原子的多个实例。

两者的最佳实践

  1. 组织相关状态:将相关状态和操作组合在一起。
  2. 使用 TypeScript:两个库都受益于 TypeScript 的类型安全。
  3. 记录你的状态结构:明确你的状态是如何组织的。
  4. 测试你的状态逻辑:为你的状态管理代码编写单元测试。

结论

在 Zustand 和 Jotai 之间选择取决于你的具体项目需求。Zustand 提供了一种集中式方法,适用于大型应用程序中复杂、相互关联的状态。Jotai 提供了一种原子模型,在细粒度响应式和最小重新渲染方面表现出色。两个库都提供轻量级、高性能的解决方案,显著改善了 Redux 的复杂性,同时保持 TypeScript 兼容性。

在做决定时,请考虑你的团队对不同状态管理模式的熟悉程度、应用程序的性能需求和状态结构。记住,你甚至可以在同一个应用程序中使用两个库,利用每个库的优势。

常见问题

是的,许多开发者使用 Zustand 管理全局应用状态,使用 Jotai 管理需要在组件树中共享的组件特定状态。

是的,如果使用得当,两者都可以扩展到大型应用程序。由于其集中式方法,Zustand 在大型团队设置中可能更容易维护。

两者都比 Redux 显著更轻量和简单。Zustand 在理念上更接近 Redux,但样板代码要少得多。Jotai 采用完全不同的方法,专注于原子状态。

Zustand 默认不需要 provider。Jotai 可以在不使用 provider 的情况下用于全局原子,但为作用域状态提供了 provider。

是的,两个库都与 Next.js 和其他 React 框架配合良好。它们提供了特定的实用程序来支持服务器端渲染。

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers