8 (800) 700-22-51

Звонок по России бесплатный

WhatsApp / Telegram

Общение через мессенджер

banner

Что такое менеджеры состояний и зачем разработчику их использовать

thumbnail

Умение управлять объемными массивами данных в сложных проектах не уникальный, но необходимый разработчику навык. Рассказываем, что такое менеджеры состояний и как Redux, MobX и другие инструменты помогают работать с состоянием компонентов в JavaScript-приложениях.

12.10.2023
  • Технологии

Что такое менеджеры состояний

Это инструмент для управления глобальным состоянием приложения. С его помощью мы можем хранить state, отслеживать его изменения, а также обеспечить более гибкую архитектуру.

Например, если покупатель добавит новые товары, удалит старые или изменит их количество в корзине онлайн-магазина, произойдет перерасчет итоговой стоимости покупок.

Без state managers работа с кодом на сложных проектах с большими объемами динамических данных будет сложнее и займет много времени.

Популярные state managers

На данный момент самые используемые менеджеры состояний — Redux, MobX и  Zustand.

Количество скачиваний state managers за последние 3 месяца по данным npm trends
Количество скачиваний state managers за последние 3 месяца по данным npm trends

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

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

Количество скачиваний state managers за последний год по данным npm trends
Количество скачиваний state managers за последний год по данным npm trends

Про Zustand и другие менеджеры мы расскажем в отдельной статье.

Redux и Mobx

Начнем обзор инструментов для работы с состоянием в JavaScript-приложениях с наиболее используемой библиотеки. 

Redux

Redux основан на трёх основных принципах:

  1. Единый источник истины. Всё состояние хранится в одном объекте — хранилище (store), что облегчает отладку и тестирование.
  2. State доступно только для чтения. Единственный способ изменить state — создать действие (action), которое описывает, что именно должно произойти. Это обеспечивает предсказуемость изменений состояния.
  3. Изменения производятся с помощью чистых функций. Чтобы изменить state, нужно использовать функцию-редуктор (reducer), которая принимает текущее состояние и действие, а затем возвращает новое state. Редукторы являются чистыми функциями, что обеспечивает простоту тестирования.

Как установить Redux

В зависимости от того, каким менеджером пакетов вы пользуетесь, введите команду "npm install redux" или "yarn add redux"

Вероятнее всего вы храните исходники в  папке src. В ней заведите отдельную папку под Redux и создайте базовую структуру для хранилища.

Подробнее про установку мы расскажем в отдельной статье.

Пример работы с Redux

Рассмотрим, как управляет состояниями Redux на примере изменения данных в стандартном счётчике на сайте.

Когда значения счётика меняются, состояние данных необходимо перезаписать
Когда значения счётика меняются, состояние данных необходимо перезаписать

Сначала создадим действия для увеличения и уменьшения значения счётчика:

const incrementAction = { type: 'INCREMENT' };
const decrementAction = { type: 'DECREMENT' };

Затем создадим редьюсер, который будет обрабатывать эти действия:

function counterReducer(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

Создадим хранилище и передадим наш  редьюсер:

import { createStore } from 'redux';
const store = createStore(counterReducer);

Теперь подпишемся на изменения состояния и обновим интерфейс приложения:

store.subscribe(() => {
  console.log('Состояние счётчика:', store.getState());
});

Осталось только отправить действия для изменения состояния:

store.dispatch(incrementAction); // Состояние счётчика: 1 
store.dispatch(incrementAction); // Состояние счётчика: 2
store.dispatch(decrementAction); // Состояние счётчика: 1

Дополнительные инструменты для работы с состоянием

На основе Redux созданы технологии, которые облегчают управление state в React-приложениях — библиотека React-Redux и пакет решений Redux Toolkit.

React-Redux облегчает работу с состояниями в приложениях на React
React-Redux облегчает работу с состояниями в приложениях на React

React-Redux. Эта библиотека предоставляет простой и эффективный способ связать состояние Redux с компонентами React.

Например, вы можете использовать функцию connect для связи вашего компонента с хранилищем Redux.  Она принимает две функции: mapStateToProps и mapDispatchToProps, которые определяют, какие части состояния и какие функции отправки действий будут доступны компоненту в виде пропcов.

Рассмотрим на примере:

Экспортируйте обернутый компонент, который получает доступ к состоянию и функциям отправки действий:

import React from 'react';
import { connect } from 'react-redux';
const Counter = (props) => {
  const { counter, increment, decrement } = props;
  return (
    <div>
      <p>Счетчик: {counter}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
};
const mapStateToProps = (state) => {
  return {
    counter: state.counter,
  };
};
const mapDispatchToProps = (dispatch) => {
  return {
    increment: () => dispatch({ type: 'INCREMENT' }),
    decrement: () => dispatch({ type: 'DECREMENT' }),
  };
};
export default connect(mapStateToProps, mapDispatchToProps)(Counter);

Теперь ваш компонент Counter имеет доступ к состоянию и функциям отправки действий из Redux хранилища через пропсы.

Redux Toolkit. Для чего он нужен и что это такое? Это официальная библиотека для React. С ее помощью можно упростить создание хранилища Redux. Для этого используем  функцию configureStore. Она автоматически настраивает миддлвэры, включая Redux Thunk, и включает инструменты разработчика Redux DevTools.

React-Redux и Redux Toolkit написаны на TypeScript, то есть типизация уже встроена. Это дополнительно облегчает работу над сервисом.

Например, Redux Toolkit включает удобную функцию для работы с асинхронными действиями, такими как запросы к API. Функция createAsyncThunk принимает строку типа действия и функцию, которая возвращает промис. 

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import api from './api';
export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
  const response = await api.get('/todos');
  return response.data;
});
const todosSlice = createSlice({
  name: 'todos',
  initialState: { items: [], status: 'idle', error: null },
  reducers: {
    // ...
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.items = action.payload;
      })
      .addCase(fetchTodos.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      });
  },
});
export const { addTodo, toggleTodo } = todosSlice.actions;
export default todosSlice.reducer;

Это позволяет автоматически создавать действия для начала, завершения и ошибок запроса.

MobX

Это библиотека для управления state, которая позволяет создавать реактивные модели данных. Она основана на наблюдении за изменениями и автоматически обновляет все зависимые от них компоненты. В отличие от Redux, который предлагает строгий и декларативный подход к управлению state, MobX обладает более гибким и менее формальным синтаксисом.

MobX отличается гибкостью и простотой в управлении
MobX отличается гибкостью и простотой в управлении
Основные принципы MobX:
  • Наблюдаемые данные (Observable): Основная идея MobX заключается в использовании наблюдаемых данных. Это позволяет библиотеке отслеживать изменения в этих данных и автоматически обновлять компоненты, которые зависят от них.
import { observable } from "mobx";
const todoStore = observable({
  todos: [],
  addTodo: function (todo) {
    this.todos.push(todo);
  },
  get todoCount() {
    return this.todos.length;
  }
});
console.log(todoStore.todoCount); // Выведет: 0
todoStore.addTodo("Купить продукты");
console.log(todoStore.todoCount); // Выведет: 1
  • Реактивные зависимости (Computed): MobX предоставляет возможность создавать вычисляемые значения, которые автоматически обновляются при изменении зависимых от них данных. Это помогает избежать повторных вычислений и улучшает производительность.
import { observable, computed } from "mobx";
const todoStore = observable({
  todos: [],
  addTodo: function (todo) {
    this.todos.push(todo);
  },
  get todoCount() {
    return this.todos.length;
  },
  get completedTodoCount() {
    return this.todos.filter(todo => todo.completed).length;
  }
});
console.log(todoStore.todoCount); // Выведет: 0
console.log(todoStore.completedTodoCount); // Выведет: 0
todoStore.addTodo({
  text: "Купить продукты",
  completed: false
});
todoStore.addTodo({
  text: "Закончить проект",
  completed: true
});
console.log(todoStore.todoCount); // Выведет: 2
console.log(todoStore.completedTodoCount); // Выведет: 1
  • Действия (Actions): Действия позволяют изменять наблюдаемые данные в MobX. Они предоставляют механизм для контролируемых и предсказуемых изменений состояния.
import { observable, action } from "mobx";
const todoStore = observable({
  todos: [],
  addTodo: action(function (todo) {
    this.todos.push(todo);
  }),
  removeTodo: action(function (todo) {
    const index = this.todos.indexOf(todo);
    if (index !== -1) {
      this.todos.splice(index, 1);
    }
  })
});
todoStore.addTodo("Купить продукты");
console.log(todoStore.todos); // Выведет: ["Купить продукты"]
todoStore.removeTodo("Купить продукты");
console.log(todoStore.todos); // Выведет: []

Redux vs MobX

Менеджеры Redux и MobX предлагают разные подходы к управлению состоянием в веб-приложениях.

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

Важно учитывать особенности проекта, уровень сложности и требования к управлению state. 

При выборе между Redux и MobX следует опираться на такие факторы:

  1. Сложность проекта. Если проект достаточно сложный и требует строгой архитектуры и явного управления state, Redux может быть лучшим выбором.
  2. Простота и гибкость. Если вы предпочитаете более простой и гибкий подход к управлению состоянием без лишнего шаблонного кода, MobX может быть предпочтительнее.
  3. Размер сообщества и экосистемы. Если вам важна поддержка сообщества и доступность инструментов, Redux имеет более развитую экосистему и широкое сообщество разработчиков.

Примеры кода со state managers и без них

Давайте рассмотрим, как менеджеры состояний упрощают разработку за счет более удобных решений.

Хук useState vs Redux
Хук useState vs Redux

Отлаживать и тестировать код проще

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

Пример кода при помощи хука useState
import React, { useState } from 'react';
const App = () => {
  const [count, setCount] = useState(0);
  const handleIncrement = () => {
    setCount(count + 1);
  };
  const handleDecrement = () => {
    setCount(count - 1);
  };
  return (
    <div>
      <h1>Counter: {count}</h1>
      <button onClick={handleIncrement}>Increment</button>
      <button onClick={handleDecrement}>Decrement</button>
    </div>
  );
};
export default App;

Мы используем useState, чтобы создать  локальное состояние count, которое обновляется с помощью функций handleIncrement и handleDecrement.

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

Пример кода с использованием Redux

Для чего нам в этом случае нужен Redux? Рассмотрим тот же код, но с использованием state manager.

javascript
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './actions';
const App = () => {
  const count = useSelector(state => state.count);
  const dispatch = useDispatch();
  const handleIncrement = () => {
    dispatch(increment());
  };
  const handleDecrement = () => {
    dispatch(decrement());
  };
  return (
    <div>
      <h1>Counter: {count}</h1>
      <button onClick={handleIncrement}>Increment</button>
      <button onClick={handleDecrement}>Decrement</button>
    </div>
  );
};
export default App;

Создаем глобальное состояние `count`, которое обрабатывается функциями `increment` и `decrement`, определенными в файле `actions.js`. Используем `useSelector` для получения значения `count` из глобального состояния и `useDispatch` для вызова функций `increment` и `decrement`.

Таким образом, мы можем легко отслеживать и тестировать state в едином месте, что упрощает разработку приложения и обнаружение ошибок.

Проще работать с большим объемом данных

С помощью state managers мы можем уменьшить нагрузку на сайт и сделать его более быстрым и отзывчивым для пользователей.

Пример кода с useState
import React, { useState } from 'react';
function App() {
  const [data, setData] = useState([]);
  const fetchData = async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts');
    const result = await response.json();
    setData(result);
  }
  return (
    <div>
      <button onClick={fetchData}>Fetch Data</button>
      <ul>
        {data.map((item) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}
export default App;

Каждый раз при обновлении state компонента React будет перерисовывать все элементы на странице, что может привести к замедлению работы приложения.

Пример кода с использованием Redux

Давайте посмотрим, как будет выглядеть код, если использовать Redux:

import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchPosts } from './actions/postActions';
function App() {
  const dispatch = useDispatch();
  const posts = useSelector((state) => state.posts.items);
  useEffect(() => {
    dispatch(fetchPosts());
  }, [dispatch]);
  return (
    <div>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}
export default App;

Создаем экшены и редукторы для получения и обработки данных, а затем используем хук useSelector для получения необходимых данных из глобального состояния.

При обновлении state компонента, React будет перерисовывать только необходимые элементы на странице, что позволяет увеличить производительность приложения.

Поможет пройти на позицию middle

State managers  —  это один инструментов, который поможет программисту улучшить навыки и стать более востребованным на рынке труда.

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

Поэтому в вакансиях на позицию middle-разработчика часто можно увидеть требование уметь работать в Redux и MobX.

Без умения работать с менеджерами состояний претендовать на позицию в крупном проекте не получится
Без умения работать с менеджерами состояний претендовать на позицию в крупном проекте не получится

Курс Redux, Redux Toolkit и MobX мы полностью посвятили state managers. В нем мы учим работать с глобальным состоянием с помощью Redux и аналогов: знакомим с теорией, рассматриваем кейсы и даем практические задания.

Курс рассчитан на 2 недели. Подойдет junior+, которые хотят прокачаться до middle+. Начать обучение вы можете бесплатно с вводного модуля.