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

Zustand использует единое хранилище, которое содержит все ваше состояние и действия. Эта модель знакома разработчикам, которые использовали Redux:

// Создание хранилища
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 });
    }
  }
}));

// Использование хранилища в компоненте
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 разбивает состояние на атомы, которые могут быть скомпонованы вместе. Это похоже на собственный useState React, но с возможностью разделения состояния между компонентами:

// Создание атомов
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 минифицированный и сжатый gzip
  • Поддержка middleware: Встроенные middleware для оптимизации производительности
  • Батчинг обновлений: Автоматически группирует обновления состояния

Профиль производительности Jotai

  • Гранулярные обновления: Ре-рендерятся только компоненты, использующие конкретные атомы
  • Размер бандла: ~3.5kB минифицированный и сжатый gzip (основной пакет)
  • Оптимизация на уровне атомов: Детальный контроль над тем, какие изменения состояния вызывают ре-рендеры
  • Производное состояние: Эффективно обрабатывает вычисляемые значения

Для быстро изменяющихся данных, которые влияют только на определенные части вашего 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. Вам нужно централизованное хранилище: Для приложений с взаимосвязанным состоянием, к которому нужно обращаться и изменять из многих компонентов.

  2. Вы переходите с Redux: API Zustand более знаком пользователям Redux, что упрощает миграцию.

  3. Вам нужен доступ к состоянию вне React: Zustand позволяет получать доступ и изменять состояние вне React-компонентов.

  4. Командная работа является приоритетом: Подход с централизованным хранилищем может быть проще в поддержке в больших командах.

  5. Вы предпочитаете явные обновления состояния: Подход Zustand делает изменения состояния более отслеживаемыми.

Когда выбирать Jotai

Jotai превосходит, когда:

  1. Вам нужна детальная реактивность: Для UI с множеством независимых частей состояния, которые часто изменяются.

  2. Вы создаете сложные формы: Атомарный подход Jotai хорошо работает для полей форм, которые нужно валидировать независимо.

  3. Вы хотите API, похожий на useState: Если вы предпочитаете API, который близко напоминает встроенные хуки React.

  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 <>{/* Провайдер не нужен */}</>;
}

Распространенные ошибки и лучшие практики

Ошибки в Zustand

  1. Фрагментация хранилища: Создание слишком большого количества хранилищ может привести к путанице в управлении состоянием.
  2. Мемоизация селекторов: Забывание мемоизации селекторов может вызвать ненужные ре-рендеры.
  3. Злоупотребление middleware: Добавление слишком большого количества middleware может повлиять на производительность.

Ошибки в Jotai

  1. Разрастание атомов: Создание слишком большого количества атомов без организации может сделать код трудным для понимания.
  2. Циклические зависимости: Создание атомов, которые зависят друг от друга циклически.
  3. Дублирование атомов: Случайное создание нескольких экземпляров одного и того же атома.

Лучшие практики для обеих

  1. Организуйте связанное состояние: Группируйте связанное состояние и действия вместе.
  2. Используйте TypeScript: Обе библиотеки выигрывают от типобезопасности TypeScript.
  3. Документируйте структуру состояния: Сделайте ясным, как организовано ваше состояние.
  4. Тестируйте логику состояния: Пишите юнит-тесты для кода управления состоянием.

Заключение

Выбор между Zustand и Jotai зависит от специфических требований вашего проекта. Zustand предлагает централизованный подход, который хорошо работает для сложного, взаимосвязанного состояния в больших приложениях. Jotai предоставляет атомарную модель, которая превосходит в детальной реактивности с минимальными ре-рендерами. Обе библиотеки предлагают легковесные, производительные решения, которые значительно улучшают сложность Redux, сохраняя при этом совместимость с TypeScript.

Учитывайте знакомство вашей команды с различными паттернами управления состоянием, потребности производительности вашего приложения и структуру состояния при принятии решения. Помните, что вы даже можете использовать обе библиотеки в одном приложении, используя каждую для того, что она делает лучше всего.

Часто задаваемые вопросы

Да, многие разработчики используют Zustand для глобального состояния приложения и Jotai для специфичного для компонентов состояния, которое нужно разделять через дерево компонентов.

Да, обе могут масштабироваться до больших приложений при правильном использовании. Zustand может быть проще в поддержке в больших командах из-за своего централизованного подхода.

Обе значительно легче и проще Redux. Zustand ближе к Redux по философии, но с гораздо меньшим количеством шаблонного кода. Jotai использует совершенно другой подход, сосредоточенный на атомарном состоянии.

Zustand не требует провайдера по умолчанию. Jotai может использоваться без провайдера для глобальных атомов, но предлагает провайдер для ограниченного состояния.

Да, обе библиотеки хорошо работают с Next.js и другими React-фреймворками. Они предоставляют специальные утилиты для поддержки серверного рендеринга.

Listen to your bugs 🧘, with OpenReplay

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