Redux Toolkit: От основ до продвинутых техник

Практический курс для освоения Redux Toolkit с нуля, ориентированный на карьерный рост. Вы научитесь настраивать хранилище, работать с асинхронными операциями, оптимизировать производительность и использовать middleware.

1. Основы Redux Toolkit: Настройка Store, создание Slices и Reducers

Основы Redux Toolkit: Настройка Store, создание Slices и Reducers

Redux Toolkit (RTK) — это современный стандарт разработки приложений с использованием Redux. Он решает основные проблемы классического Redux: сложность настройки хранилища, необходимость установки множества дополнительных пакетов и большое количество шаблонного кода (boilerplate). RTK предоставляет набор инструментов, которые упрощают написание эффективного и понятного кода.

!Однонаправленный поток данных: действия из компонента меняют состояние в Store, которое затем обновляет компонент

Установка зависимостей

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

  • @reduxjs/toolkit: содержит основные методы для создания слайсов, хранилища и работы с данными.
  • react-redux: обеспечивает интеграцию Redux-хранилища с компонентами React (провайдеры, хуки).
  • Создание хранилища (Store)

    В классическом Redux настройка хранилища требовала ручной конфигурации middleware, DevTools и объединения редьюсеров. В RTK для этого используется функция configureStore.

    Эта функция автоматически:

  • Объединяет ваши редьюсеры.
  • Добавляет полезные middleware (например, redux-thunk для асинхронности).
  • Включает расширение Redux DevTools для браузера.
  • Добавляет проверки на типичные ошибки (например, мутации состояния вне Immer).
  • Создадим файл store.js:

    Объект reducer внутри configureStore определяет структуру глобального состояния. В данном примере состояние будет иметь поле counter, управляемое соответствующим редьюсером.

    Концепция Слайсов (Slices)

    Слайс (Slice) — это ключевая концепция Redux Toolkit. Это модуль, который объединяет в себе логику обработки состояния (редьюсеры) и действия (actions), связанные с конкретной функциональностью приложения (например, счетчик, пользователь, список задач).

    Раньше разработчикам приходилось создавать отдельные файлы для типов действий (constants), генераторов действий (action creators) и редьюсеров. Функция createSlice генерирует всё это автоматически.

    Создание слайса

    Рассмотрим пример создания слайса для счетчика в файле features/counter/counterSlice.js:

    Магия Immer и мутабельность

    Одним из самых сложных правил Redux всегда был запрет на прямую мутацию состояния. Вы были обязаны возвращать новый объект, используя операторы spread (...state).

    Redux Toolkit использует библиотеку Immer под капотом. Это позволяет писать код так, будто вы меняете состояние напрямую (state.value += 1), но в результате Immer безопасно создает новую копию состояния (immutable update). Это значительно сокращает код и делает его более читаемым.

    Важно помнить: такой синтаксис допустим только внутри createSlice (или createReducer).

    Подключение Store к React

    Чтобы React-компоненты могли взаимодействовать с Redux, необходимо обернуть корневой компонент приложения в Provider. Это делается обычно в файле index.js или main.jsx.

    Компонент Provider принимает проп store и делает его доступным для любого вложенного компонента через хуки.

    Использование состояния и действий в компонентах

    Для взаимодействия с Redux в функциональных компонентах используются два основных хука из библиотеки react-redux:

  • useSelector — для чтения данных из хранилища.
  • useDispatch — для отправки действий (actions) для изменения состояния.
  • Чтение данных с useSelector

    Хук useSelector принимает функцию-селектор, которая получает всё состояние (state) и возвращает нужную его часть.

    Когда значение state.counter.value изменится, компонент автоматически перерисуется.

    Отправка действий с useDispatch

    Хук useDispatch возвращает функцию dispatch, которую нужно вызывать с объектом действия (action). Действия мы импортируем из созданного ранее слайса.

    Когда пользователь нажимает кнопку, dispatch(increment()) отправляет действие в Store. Store находит соответствующий редьюсер в слайсе, выполняет логику обновления, и новое состояние распространяется по приложению.

    Передача данных через Payload

    Часто действию нужно передать дополнительные данные (например, на сколько увеличить счетчик или текст новой задачи). В createSlice эти данные доступны через action.payload.

    В компоненте это выглядит так:

    Здесь amount попадет в action.payload внутри редьюсера incrementByAmount.

    Итоги

  • configureStore упрощает создание хранилища, автоматически настраивая DevTools и middleware.
  • createSlice объединяет логику действий и редьюсеров в одном месте, устраняя необходимость в большом количестве шаблонного кода.
  • Immer позволяет писать код изменения состояния в "мутабельном" стиле внутри createSlice, что делает редьюсеры более понятными.
  • Provider связывает Redux Store с React-приложением.
  • useSelector и useDispatch — основные хуки для чтения состояния и запуска изменений из компонентов.
  • 2. Интеграция с React: Провайдеры и хуки useSelector, useDispatch

    Интеграция с React: Провайдеры и хуки useSelector, useDispatch

    После настройки хранилища (Store) и создания слайсов (Slices) логика Redux существует изолированно от пользовательского интерфейса. Чтобы React-приложение могло реагировать на изменения состояния и инициировать их, используется библиотека react-redux. Она предоставляет механизмы для связывания глобального состояния с деревом компонентов React.

    !Циклический поток данных: чтение через селекторы и обновление через диспетчер действий

    Провайдер (Provider)

    Первым шагом интеграции является внедрение хранилища в дерево компонентов React. Для этого используется компонент Provider. Он использует React Context API под капотом, чтобы сделать Redux Store доступным для любого вложенного компонента, независимо от глубины вложенности.

    Обычно это делается в корневом файле приложения (например, main.jsx или index.js), чтобы всё приложение имело доступ к состоянию.

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

    Чтение данных: хук useSelector

    Для получения данных из хранилища в функциональных компонентах используется хук useSelector. Он принимает в качестве аргумента функцию-селектор.

    Селектор — это чистая функция, которая принимает всё глобальное состояние (state) и возвращает ту его часть, которая нужна конкретному компоненту.

    Синтаксис и пример

    Предположим, наше глобальное состояние выглядит так:

    Чтобы получить значение счетчика в компоненте:

    Механизм повторного рендеринга

    useSelector автоматически подписывается на обновления Redux Store. Всякий раз, когда действие (action) было отправлено (dispatched), useSelector запускает переданную функцию-селектор снова.

    Логика обновления компонента опирается на строгое равенство. Если результат селектора изменился по сравнению с предыдущим вызовом, компонент принудительно перерисовывается (re-render).

    Формально условие рендеринга можно записать так:

    Где — результат работы селектора после обновления состояния, а — результат предыдущего вызова селектора. Если они не равны, React обновляет компонент.

    Оптимизация выборки данных

    Важно выбирать из состояния только те данные, которые действительно нужны компоненту. Если вы вернете весь объект состояния, компонент будет перерисовываться при любом изменении в Store, даже если данные, касающиеся этого компонента, не менялись.

    Плохо:

    Хорошо:

    Изменение данных: хук useDispatch

    Для изменения состояния в Redux используется отправка действий (dispatching actions). Хук useDispatch возвращает ссылку на функцию dispatch из Redux Store.

    Использование

  • Импортируем хук useDispatch и генераторы действий (action creators) из слайса.
  • Вызываем хук внутри компонента для получения функции dispatch.
  • Вызываем dispatch, передавая ему результат вызова генератора действий.
  • Передача данных (Payload)

    Если действие требует передачи данных (например, увеличение на заданное число), эти данные передаются аргументом в генератор действия. Redux Toolkit автоматически поместит их в поле payload объекта действия.

    В этом примере при нажатии кнопки в Store отправится объект вида: { type: 'counter/incrementByAmount', payload: 5 } (если введено 5).

    Полный пример компонента

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

    В данном примере:

  • При загрузке компонент получает isDark из Store.
  • Пользователь нажимает кнопку.
  • dispatch(toggleTheme()) отправляет действие.
  • Redux обновляет state.theme.isDarkMode.
  • useSelector замечает изменение значения.
  • Компонент ThemeToggler перерисовывается с новыми стилями.
  • Распространенные ошибки

  • Вызов dispatch при рендеринге. Никогда не вызывайте dispatch прямо в теле компонента без useEffect или обработчиков событий. Это приведет к бесконечному циклу обновлений.
  • * ~~dispatch(loadData())~~ (в теле функции) * Правильно: useEffect(() => { dispatch(loadData()) }, [])

  • Мутация данных в селекторе. Селекторы должны быть чистыми функциями. Не изменяйте state внутри useSelector.
  • Забытый Provider. Если ваши компоненты не видят Store, проверьте, обернуто ли приложение в <Provider> в корневом файле.
  • Итоги

  • Provider — обязательный компонент-обертка, который передает Redux Store всему дереву React-компонентов через контекст.
  • useSelector используется для получения данных. Он принимает функцию-селектор и вызывает перерисовку компонента, если возвращаемое значение изменилось.
  • useDispatch предоставляет функцию dispatch для отправки действий (actions), которые инициируют изменение состояния.
  • Селекторы должны выбирать минимально необходимый набор данных, чтобы избежать лишних рендеров.
  • Связка react-redux позволяет компонентам быть реактивными: интерфейс автоматически синхронизируется с глобальным состоянием.