Урок 19: Сравнение с другими менеджерами состояний (сontext API, MobX, Zustand)

Мы подходим к финальной прямой нашего большого курса управления состоянием. Сегодня у нас особенный урок. Мы ненадолго отложим в сторону код и поговорим о выборе.

Пройдя 18 уроков, вы стали чувствовать себя уверенно с Redux Toolkit. Вы знаете, как создавать слайсы, работать с асинхронностью через RTK Query и тестировать свою логику. Но возникает закономерный вопрос, а всегда ли нам нужен именно Redux? Ответ нет. В мире фронтенда нет серебряной пули и каждый инструмент занимает свою нишу.

Сегодня мы проведем честное и детальное сравнение Redux Toolkit с тремя другими популярными решениями, встроенным Context API, объектно-ориентированным MobX и минималистичным Zustand. Наша цель не доказать, что один инструмент лучше другого, а понять, когда и почему стоит выбрать тот или иной вариант. Это знание сделает вас более сильным и гибким разработчиком, способным принимать взвешенные архитектурные решения.

Context API

Давайте начнем с самого простого и доступного инструмента, который буквально встроен в React, это Context API. Он предназначен для передачи данных через дерево компонентов без необходимости передавать пропсы на каждом уровне (так называемый «пропс дриллинг»).

Context API отлично справляется с задачами, где данные меняются нечасто. Ярчайшие примеры, это  тема оформления (светлая/темная), выбранный язык интерфейса (i18n), информация о текущем аутентифицированном пользователе (которая обновляется лишь в момент входа или выхода).

Почему же его не стоит использовать для часто изменяющихся данных? Все дело в механике работы. Когда значение в провайдере контекста изменяется, React перерисовывает всех потребителей этого контекста, независимо от того, изменилась ли та часть данных, которую они используют. В большом приложении с сложным состоянием это может легко привести к серьезным проблемам с производительностью. Вы можете пытаться оптимизировать это с помощью React.memo и разбиения контекстов, но это добавляет сложности и не является панацеей.

Давайте создадим контекст для темы. Это его идеальная среда обитания.

jsx
// ThemeContext.js
import React, { createContext, useContext, useState } from 'react';

const ThemeContext = createContext();

export const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

// Кастомный хук для удобства
export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
};

// App.js
import { ThemeProvider } from './ThemeContext';
import Header from './Header';
import MainContent from './MainContent';

function App() {
  return (
    <ThemeProvider>
      <Header />
      <MainContent />
    </ThemeProvider>
  );
}

// Header.js
import React from 'react';
import { useTheme } from './ThemeContext';

const Header = () => {
  const { theme, toggleTheme } = useTheme();
  console.log('Header rendered with theme:', theme); // Этот лог сработает только при смене темы

  return (
    <header className={theme}>
      <h1>My App</h1>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </header>
  );
};

export default Header;

Плюсы Context API:

  1. Встроен в React. Не требует установки дополнительных библиотек. Нулевая зависимость.

  2. Простота для статичных значений. Идеален для распространения данных, которые меняются редко (тема, локаль, пользователь).

Минусы Context API:

  1. Проблемы с производительностью. Не предназначен для часто изменяющихся данных. Изменение контекста приводит к перерисовке всех дочерних компонентов, подписанных на него.

  2. Сложность с сложной логикой. Отсутствует встроенная поддержка для обработки сайд-эффектов, асинхронных действий или разработки инструментов, что приводит к необходимости писать много дополнительного кода.

MobX

Если Redux следует за функциональным программированием и иммутабельностью, то MobX это его объектно-ориентированный собрат, который обожает мутации. Его идея заключается в создании «наблюдаемых» состояний. Когда вы изменяете такое состояние, все, что от него зависит (например компоненты React), автоматически обновляется.

В MobX вы создаете классы или объекты, помечаете их поля как observable, а методы, которые изменяют состояние, как actions. Компоненты, которые используют эти наблюдаемые поля, автоматически становятся «реактивными» и перерисовываются при любом изменении. Это приводит к отличной производительности «из коробки», так как система зависимостей MobX точно знает, что и когда нужно обновлять.

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

Давайте создадим тот же счетчик, но с использованием MobX и React-биндинга mobx-react-lite.

bash
npm install mobx mobx-react-lite
jsx
// CounterStore.js
import { makeAutoObservable } from 'mobx';

class CounterStore {
  count = 0;

  constructor() {
    // Делаем все свойства класса наблюдаемыми, а методы - действиями
    makeAutoObservable(this);
  }

  increment() {
    this.count += 1;
  }

  decrement() {
    this.count -= 1;
  }
}

// Создаем экземпляр хранилища и экспортируем его
export const counterStore = new CounterStore();
jsx
// Counter.jsx
import React from 'react';
import { observer } from 'mobx-react-lite';
import { counterStore } from './CounterStore';

const Counter = observer(() => {
  return (
    <div>
      <h2>Counter: {counterStore.count}</h2>
      <button onClick={() => counterStore.increment()}>+</button>
      <button onClick={() => counterStore.decrement()}>-</button>
    </div>
  );
});

export default Counter;

Плюсы MobX:

  1. Отличная производительность. Тонкая система подписки гарантирует, что перерисовываются только те компоненты, которые зависят от изменившихся данных.

  2. Минимальный бойлерплейт и интуитивный ООП-подход. Код часто читается как обычное изменение объектов, что может быть проще для разработчиков, пришедших из бэкенда.

Минусы MobX:

  1. Меньшая предсказуемость. Из-за автоматической реактивности сложнее отслеживать поток данных и понимать, что именно привело к изменению состояния.

  2. Меньшая явность. Отладка может быть сложнее по сравнению с Redux, где каждый шаг логируется и виден в DevTools. Сообщество и экосистема хоть и большие, но уступают Redux.

Zustand

Представьте себе всю мощь глобального состояния, но без всей церемонии и шаблонного кода, присущего «классическому» Redux. Это Zustand (с немецкого значит «состояние»). Его API настолько прост, что его можно изучить за 5 минут.

Вы создаете хранилище с помощью функции create, внутри которой определяете начальное состояние и методы для его изменения. Вам не нужны провайдеры, чтобы обернуть всё приложение! Вы просто импортируете хук вашего хранилища в любом компоненте и используете его. Zustand автоматически позаботится о подписках и отписках.

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

Давайте создадим сейчас хранилище для списка задач (todos) на Zustand.

bash
npm install zustand
jsx
// useTodoStore.js
import { create } from 'zustand';

export const useTodoStore = create((set, get) => ({
  todos: [],
  addTodo: (text) => {
    const newTodo = { id: Date.now(), text, completed: false };
    // `set` функция объединяет состояние (не нужно возвращать полное состояние, как в Redux)
    set((state) => ({ todos: [...state.todos, newTodo] }));
  },
  toggleTodo: (id) => {
    set((state) => ({
      todos: state.todos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      ),
    }));
  },
  // Можно использовать `get` для доступа к текущему состоянию внутри действия
  getCompletedTodosCount: () => {
    const todos = get().todos;
    return todos.filter(todo => todo.completed).length;
  },
}));
jsx
// TodoList.jsx
import React, { useState } from 'react';
import { useTodoStore } from './useTodoStore';

const TodoList = () => {
  const { todos, addTodo, toggleTodo, getCompletedTodosCount } = useTodoStore();
  const [text, setText] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim()) {
      addTodo(text);
      setText('');
    }
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input value={text} onChange={(e) => setText(e.target.value)} />
        <button type="submit">Add Todo</button>
      </form>
      <ul>
        {todos.map(todo => (
          <li key={todo.id} onClick={() => toggleTodo(todo.id)} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            {todo.text}
          </li>
        ))}
      </ul>
      <div>Completed: {getCompletedTodosCount()}</div>
    </div>
  );
};

export default TodoList;

Плюсы Zustand:

  1. Невероятно простой API. Минимум шаблонного кода. Нет необходимости в провайдерах, действиях, редьюсерах или селекторах (в базовом usage).

  2. Отличная производительность и размер. Библиотека очень легковесна и, как и другие решения, подписывает компоненты только на те данные, которые они используют.

Минусы Zustand:

  1. Меньше структурности для больших проектов. Свобода, которую дает Zustand, может привести к неорганизованности кода в очень больших командах и проектах, если не установить строгие внутренние правила.

  2. Меньше встроенных возможностей. Для таких вещей, как кэширование данных и работа с API, вам нужно искать сторонние решения или писать свою логику, в то время как RTK Query предоставляет это «из коробки».

Redux Toolkit

И наконец то, наш главный инструмент это Redux Toolkit (RTK). Его основная идея в предсказуемости и явности. Каждое изменение состояния описывается явным действием (action). Вы точно знаете, что произошло в приложении и как оно отреагировало на это. Это делает код легко тестируемым, отлаживаемым и предсказуемым, особенно в больших командах.

Благодаря Redux DevTools вы можете буквально «путешествовать во времени» по состоянию вашего приложения, что неоценимо при отладке. RTK, будучи официальным пакетом, вобрал в себя лучшие практики сообщества. Он решает главные проблемы «ванильного» Redux, это шаблонность и сложность настройки, предоставляя такие инструменты, как createSlicecreateAsyncThunk и RTK Query.

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

Плюсы Redux Toolkit:

  1. Предсказуемость и отладка. Один из лучших инструментов для отладки на рынке. Позволяет логировать действия, «путешествовать во времени» (time-travel debugging) и легко воспроизводить баги.

  2. Структура и масштабируемость. Четкое разделение на действия, редьюсеры и селекторы обеспечивает хорошую структуру кода, которая масштабируется в больших командах и проектах. RTK Query предоставляет первоклассное решение для кэширования и синхронизации данных с сервером.

Минусы Redux Toolkit:

  1. Относительно большой объем шаблонного кода. Даже с RTK по сравнению с Zustand или MobX, вам все равно нужно писать больше кода (слайсы, селекторы, диспатч).

  2. Кривая обучения. Концепции хранилища, действий, редьюсеров и middleware, а также иммутабельности, могут быть сложны для новичков.

Практическое задание для закрепления

Чтобы почувствовать разницу, я предлагаю вам небольшое упражнение. Не реализовывать всё, а просто продумать.

Вам нужно создать небольшой виджет «Избранное» для интернет-магазина. Пользователь может добавлять товары в избранное с разных страниц сайта и значок с количеством избранных товаров должен отображаться в шапке.

  1. Context API. Опишите, с какими проблемами вы можете столкнуться, если реализуете это на Context API, особенно когда количество товаров станет большим.

  2. MobX. Создайте простой класс-хранилище FavoritesStore с наблюдаемым массивом items и действиями addItem и removeItem.

  3. Zustand. Напишите заготовку хранилища useFavoritesStore для этой же задачи. Обратите внимание на простоту API.

  4. Redux Toolkit. Спланируйте структуру: какой будет слайс? Какие экшены? Какой селектор для подсчета количества?

Сравните свои ощущения от каждого подхода. Какой показался вам самым простым для такой задачи? Какой вы бы выбрали для реального большого проекта, где виджет «Избранное» лишь одна из сотни фич?

Итоги 19-го урока и рекомендации

Итак, давайте резюмируем наш честный разбор:

  • Context API. Ваш выбор для статичных или редко меняющихся глобальных данных (тема, локаль, пользователь). Не используйте его для всего состояния приложения.

  • MobX. Отличный выбор, если вы любите объектно-ориентированный подход, цените реактивность и работаете над приложением, где производительность критична.

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

  • Redux Toolkit. Бесспорный лидер для больших, сложных и долгосрочных проектов с большой командой разработчиков, где важны предсказуемость, возможности отладки, структура и богатая экосистема (особенно RTK Query).

Не существует «победителя» в этом сравнении. Есть правильный инструмент для правильной задачи и правильной команды. Теперь, обладая этим знанием, вы можете делать осознанный выбор.

Это был предпоследний шаг в нашем курсе. В следующем, заключительном уроке, мы соберем все полученные знания воедино и создадим мини-приложение, это будет интерфейс для блога.

Полный курс с уроками по Redux и Redux Toolkit для начинающих вы можете найти здесь — https://max-gabov.ru/redux-dlya-nachinaushih.

Поделиться статьей:
Поддержать автора блога

Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.

Персональные рекомендации
Оставить комментарий