Освоение Redux: От основ до Redux Toolkit

Этот курс предназначен для изучения библиотеки управления состоянием Redux в экосистеме React. Вы пройдете путь от понимания архитектуры Flux до использования современного инструментария Redux Toolkit и обработки асинхронных операций.

1. Введение в Redux: Архитектура Flux, Store, Actions и чистые функции Reducers

Введение в Redux: Архитектура Flux, Store, Actions и чистые функции Reducers

Добро пожаловать в курс «Освоение Redux». Если вы когда-либо создавали сложные приложения на JavaScript (особенно на React), вы, вероятно, сталкивались с проблемой управления состоянием. Данные разбросаны по разным компонентам, передача пропсов на десять уровней вниз превращается в кошмар, а отслеживание того, кто и когда изменил данные, становится невозможным. Именно здесь на сцену выходит Redux.

В этой первой статье мы разберем фундаментальные концепции Redux, не привязываясь к конкретному UI-фреймворку. Мы поймем, откуда растут корни этой библиотеки, изучив архитектуру Flux, и детально разберем три кита Redux: Store, Actions и Reducers.

Проблема управления состоянием

Представьте, что ваше приложение — это сложная система водопровода. Вода (данные) должна поступать в разные части дома (компоненты). В простых приложениях вы можете просто носить воду ведрами (props). Но когда дом превращается в небоскреб, бегать с ведрами становится неэффективно и опасно — вода проливается, ведра теряются, и никто не знает, сколько воды осталось в главном резервуаре.

Redux предлагает установить централизованную насосную станцию и четкую систему труб, где вода течет только в одном направлении. Это делает поведение системы предсказуемым.

Архитектура Flux: Прародитель Redux

Чтобы понять Redux, нужно взглянуть на его предшественника — архитектуру Flux, разработанную Facebook. Flux — это не библиотека, а паттерн проектирования, созданный для решения проблем двунаправленного потока данных (как в MVC), который часто приводил к каскадным обновлениям и непредсказуемым багам.

Ключевая идея Flux — однонаправленный поток данных (Unidirectional Data Flow).

!Схема однонаправленного потока данных в архитектуре Flux

Основные компоненты Flux:

  • Actions (Действия): События, происходящие в приложении.
  • Dispatcher (Диспетчер): Центральный хаб, который получает действия и рассылает их всем зарегистрированным хранилищам.
  • Stores (Хранилища): Содержат состояние приложения и логику его изменения.
  • View (Представление): Компоненты интерфейса, которые подписываются на изменения в Stores.
  • Redux развил эту идею, упростив её. В отличие от Flux, где может быть много Stores и сложный Dispatcher, Redux использует один Store и полагается на чистые функции для изменения состояния.

    Три принципа Redux

    Redux строится на трех фундаментальных принципах:

  • Единственный источник правды (Single Source of Truth): Все состояние вашего приложения хранится в одном объекте-дереве внутри одного Store.
  • Состояние доступно только для чтения (State is Read-Only): Единственный способ изменить состояние — это отправить (dispatch) действие (Action), описывающее, что произошло.
  • Изменения делаются чистыми функциями (Changes are made with Pure Functions): Для описания того, как действия трансформируют дерево состояния, вы пишете редьюсеры (Reducers).
  • Давайте разберем каждый элемент подробнее.

    Store: Сердце приложения

    Store (Хранилище) — это объект, который держит состояние приложения. В Redux есть только один Store. Это как банковское хранилище: все деньги (данные) лежат там, и вы не можете просто зайти и взять их, не пройдя через кассира.

    Store выполняет следующие задачи:

    * Хранит состояние приложения. * Предоставляет доступ к состоянию через метод getState(). * Позволяет обновлять состояние через метод dispatch(action). * Позволяет подписываться на изменения через метод subscribe(listener).

    Actions: Намерение изменить данные

    Action (Действие) — это обычный JavaScript-объект, который описывает, что произошло. Это единственный источник информации для Store. Actions — это как квитанция, которую вы передаете кассиру в банке: «Я хочу положить 100 рублей».

    Обязательное требование к Action — наличие поля type. Оно обычно задается строковой константой.

    Пример Action:

    Здесь type говорит о типе события, а payload (полезная нагрузка) содержит дополнительные данные, необходимые для выполнения действия. Поле payload не является обязательным стандартом Redux, но является общепринятым соглашением (FSA — Flux Standard Action).

    Reducers: Логика изменений

    Если Action описывает, что произошло, то Reducer (Редьюсер) описывает, как меняется состояние приложения в ответ на это действие.

    Технически, Reducer — это чистая функция, которая принимает текущее состояние и действие, а возвращает новое состояние.

    Математически это можно выразить следующей формулой:

    Где — новое состояние приложения, — функция-редьюсер, — текущее состояние, а — действие (Action).

    Что такое «Чистая функция»?

    Понимание чистых функций критически важно для работы с Redux. Функция считается чистой, если она соблюдает два правила:

  • Детерминированность: При одних и тех же входных данных она всегда возвращает один и тот же результат.
  • Отсутствие побочных эффектов: Она не изменяет внешние переменные, не делает запросов к API, не изменяет DOM и не мутирует (изменяет) свои аргументы.
  • !Сравнение чистой функции и функции с побочными эффектами

    Иммутабельность (Неизменяемость)

    В Redux запрещено изменять состояние напрямую. Вы никогда не должны писать что-то вроде state.value = 5. Вместо этого редьюсер должен вернуть новый объект состояния, который содержит изменения.

    Пример простейшего редьюсера:

    Обратите внимание на использование оператора расширения (spread operator) ...state. Это распространенный способ создания копии объекта в JavaScript для соблюдения принципа иммутабельности.

    Поток данных в Redux

    Теперь, когда мы знаем все компоненты, давайте соберем их вместе и посмотрим на жизненный цикл данных в Redux приложении. Этот процесс строго однонаправленный:

  • Вызов действия: Что-то происходит в приложении (пользователь нажал кнопку). Создается объект Action.
  • Dispatch: Action отправляется в Store с помощью метода store.dispatch(action).
  • Обработка в Reducer: Store вызывает функцию Reducer, передавая ей текущее дерево состояния и полученный Action.
  • Обновление: Reducer возвращает новое дерево состояния. Store сохраняет его.
  • Уведомление: Store уведомляет всех подписчиков (обычно это UI-компоненты), что состояние изменилось. Компоненты перерисовываются с новыми данными.
  • !Жизненный цикл данных в Redux: от действия пользователя до обновления интерфейса

    Почему это работает?

    Такая жесткая структура может показаться избыточной для простых задач. Зачем писать Actions и Reducers, если можно просто изменить переменную?

    Ответ кроется в предсказуемости и отладке. Поскольку редьюсеры — это чистые функции, вы всегда можете воспроизвести любую ошибку, просто зная начальное состояние и последовательность действий. Это открывает возможности для таких инструментов, как «путешествие во времени» (Time Travel Debugging), когда вы можете отматывать состояние приложения назад и вперед, чтобы понять, что пошло не так.

    Заключение

    Мы разобрали фундамент Redux. Теперь вы знаете, что Store — это единственное хранилище, Actions — это вестники событий, а Reducers — это чистые функции, которые решают, как изменится мир вашего приложения. В следующей статье мы перейдем от теории к практике и настроим наше первое Redux-окружение.

    2. Современный Redux: Настройка хранилища через configureStore и создание слайсов с Redux Toolkit

    Современный Redux: Настройка хранилища через configureStore и создание слайсов с Redux Toolkit

    В предыдущей статье мы погрузились в фундаментальные концепции Redux. Мы разобрали, как работают Actions, Reducers и Store, и даже написали простую логику обновления состояния вручную. Вы наверняка заметили, что «классический» Redux требует написания большого количества шаблонного кода (boilerplate). Бесконечные константы для типов действий, громоздкие конструкции switch-case в редьюсерах и ручное объединение редьюсеров — всё это усложняет разработку.

    Сегодня мы переходим к современному стандарту разработки — Redux Toolkit (RTK). Это официальный набор инструментов, который делает работу с Redux проще, быстрее и безопаснее. Мы научимся настраивать хранилище одной функцией и создавать логику приложения с помощью «слайсов».

    Почему Redux Toolkit?

    До появления Redux Toolkit разработчики сталкивались с тремя главными проблемами:

  • Сложная настройка: Чтобы заставить Redux работать с DevTools и асинхронными действиями, приходилось устанавливать несколько пакетов и писать сложный конфигурационный код.
  • Много шаблонного кода: Для одного действия нужно было создать константу типа, генератор действия (action creator) и обработчик в редьюсере.
  • Необходимость дополнительных библиотек: Для асинхронности нужно было добавлять redux-thunk или redux-saga, для селекторов — reselect.
  • Redux Toolkit решает эти проблемы, предоставляя готовые абстракции. Это «батарейки в комплекте» для Redux.

    Настройка хранилища: configureStore

    В классическом Redux мы использовали функцию createStore. В современном подходе она считается устаревшей. На её место пришла функция configureStore.

    configureStore — это обертка над стандартным createStore, которая автоматически настраивает:

    * Redux DevTools Extension: Инструменты разработчика включаются автоматически. * Middleware (Промежуточное ПО): По умолчанию подключаются redux-thunk (для асинхронности) и проверки на случайные мутации состояния.

    Пример настройки Store

    Давайте посмотрим, как выглядит создание хранилища в современном приложении. Обычно этот код находится в файле store.js или app/store.js.

    Обратите внимание на объект reducer внутри конфигурации. В классическом Redux нам приходилось использовать combineReducers, чтобы объединить несколько редьюсеров в один корневой. configureStore делает это за нас автоматически. Если вы передадите объект с редьюсерами, он сам создаст корневой редьюсер.

    !Схематичное изображение того, как configureStore автоматически собирает необходимые инструменты и редьюсеры в готовое хранилище

    Слайсы: Революция в организации кода

    В классическом Redux логика часто размазывалась по разным файлам: actions.js, constants.js, reducers.js. Это затрудняло навигацию. Redux Toolkit вводит концепцию Slice (Слайс).

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

    Для создания слайса используется функция createSlice. Она принимает объект с настройками и автоматически генерирует:

  • Action Creators: Функции для создания действий.
  • Action Types: Константы типов действий (например, counter/increment).
  • Reducer: Функцию-редьюсер для обработки действий.
  • Создание первого слайса

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

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

    Вы наверняка заметили странность в коде выше:

    В прошлой статье мы строго говорили: «Никогда не мутируйте состояние! Всегда возвращайте новый объект!». Почему же здесь мы пишем state.value += 1?

    Redux Toolkit использует под капотом библиотеку Immer. Эта библиотека позволяет писать код так, будто вы изменяете данные напрямую (мутируете их), но на самом деле она отслеживает все изменения и создает новую неизменяемую копию состояния за вас.

    Это работает благодаря JavaScript-объекту Proxy. Immer создает «черновик» (Draft) текущего состояния, вы вносите в него изменения, а затем Immer берет этот черновик и формирует на его основе финальное неизменяемое состояние.

    !Визуализация процесса трансформации состояния через Immer: от текущего состояния через черновик к новому иммутабельному состоянию

    Важно: Это работает только внутри createSlicecreateReducer). Если вы пишете редьюсер вручную без RTK, мутировать состояние по-прежнему запрещено.

    Подключение к React приложению

    Теперь, когда у нас есть настроенный store и созданный slice, нам нужно подключить их к нашему React-приложению. Этот процесс остался похожим на классический подход, но давайте освежим его в памяти.

    Provider

    Для того чтобы любой компонент приложения мог получить доступ к Redux, мы должны обернуть всё приложение в компонент Provider из библиотеки react-redux.

    В файле index.js (или main.jsx):

    Теперь store доступен во всем дереве компонентов.

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

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

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

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

    useSelector автоматически подписывается на обновления Store. Если state.counter.value изменится, компонент CounterDisplay автоматически перерисуется.

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

    Чтобы изменить состояние, нам нужно отправить действие. Для этого мы получаем функцию dispatch и вызываем её, передавая результат выполнения наших Action Creators, которые мы экспортировали из слайса.

    Обратите внимание: мы вызываем increment(), чтобы получить объект действия (например, { type: 'counter/increment' }), и передаем этот объект в dispatch.

    Сравнение подходов

    Давайте кратко сравним, что мы выиграли, перейдя на Redux Toolkit.

    | Характеристика | Классический Redux | Redux Toolkit | | :--- | :--- | :--- | | Настройка Store | Ручная композиция middleware, DevTools | configureStore с готовыми настройками | | Логика | Раздельные файлы для типов, экшенов, редьюсеров | Один файл createSlice | | Изменение State | Сложное копирование (...state, Object.assign) | Прямая мутация (благодаря Immer) | | Типы действий | Строковые константы вручную | Генерируются автоматически (name/reducerName) |

    Заключение

    Redux Toolkit кардинально меняет опыт разработки. Он убирает рутину, позволяя сосредоточиться на логике приложения. Мы научились:

  • Создавать хранилище с помощью configureStore.
  • Организовывать логику в слайсы через createSlice.
  • Писать лаконичный код обновления состояния, используя возможности Immer.
  • Подключать всё это к React-компонентам через хуки.
  • В следующей статье мы разберем более сложную тему: как работать с асинхронными данными (запросами к серверу) в Redux Toolkit и что такое Thunks.

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

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

    В предыдущих статьях мы проделали большой путь: разобрали теоретические основы Redux, архитектуру Flux и научились создавать современное хранилище с помощью Redux Toolkit, используя configureStore и createSlice. Однако, пока что наш Redux существует в вакууме. У нас есть настроенное хранилище, но наш пользовательский интерфейс на React ничего о нем не знает.

    В этой статье мы построим мост между логикой управления состоянием и визуальным интерфейсом. Мы изучим официальную библиотеку react-redux, которая является стандартом индустрии для связывания этих двух технологий. Мы разберем, как «прокинуть» хранилище во все компоненты приложения и как точечно извлекать данные и отправлять действия с помощью хуков.

    Библиотека React-Redux

    Сам по себе Redux — это библиотека, независимая от фреймворка. Вы можете использовать её с Vue, Angular или даже с ванильным JavaScript. Чтобы Redux эффективно работал с React, нам нужен связующий слой, который будет управлять подписками на обновления хранилища и оптимизировать перерисовку компонентов.

    Эту роль выполняет пакет react-redux. Если вы используете шаблон Create React App с Redux или Vite с шаблоном Redux, этот пакет уже установлен. Если нет, его добавляют командой:

    Провайдер: Окутываем приложение данными

    Первый шаг интеграции — сделать так, чтобы наше хранилище (store) стало доступным для любого компонента в дереве приложения, будь то кнопка в подвале сайта или шапка профиля на самом верху.

    В React для передачи данных сверху вниз без явной передачи пропсов (props drilling) используется контекст (Context API). Библиотека react-redux предоставляет компонент <Provider>, который использует этот механизм под капотом.

    Настройка Provider

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

    Теперь, когда приложение обернуто в <Provider>, любой вложенный компонент может получить доступ к состоянию Redux. Это работает магически, но за этой магией стоит строгая логика подписок.

    !Визуализация того, как Provider делает хранилище доступным для всего дерева компонентов

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

    В старых версиях Redux (до появления хуков) использовалась функция высшего порядка connect. Сейчас этот подход считается устаревшим для функциональных компонентов. Современный стандарт — это хук useSelector.

    useSelector позволяет извлечь данные из состояния хранилища Redux.

    Как это работает?

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

    Жизненный цикл и перерисовка

    useSelector — это умный хук. Он автоматически подписывается на обновления Redux Store.

  • Когда в Redux происходит действие (action), useSelector запускает вашу функцию-селектор снова.
  • Он сравнивает новый результат с предыдущим результатом.
  • Если результат изменился, хук заставляет компонент React перерисоваться (re-render) с новыми данными.
  • Если результат остался тем же, компонент не перерисовывается.
  • Важность ссылочной целостности

    Сравнение результатов происходит по ссылке (строгое равенство ===). Это критически важно понимать для оптимизации производительности.

    Рассмотрим пример плохого использования:

    В примере выше функция-селектор каждый раз создает новый объект { count: ..., user: ... }. Даже если данные внутри не изменились, ссылка на объект будет новой. Это заставит компонент перерисовываться после каждого действия в Redux, даже если оно не касалось счетчика или пользователя.

    Правильный подход — вызывать useSelector несколько раз для каждого примитивного значения или использовать мемоизированные селекторы (о которых мы поговорим в будущих статьях про оптимизацию).

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

    Чтобы изменить состояние, мы не можем просто присвоить переменной новое значение. Мы должны отправить (dispatch) действие (action). Для этого используется хук useDispatch.

    Этот хук не принимает аргументов и возвращает ссылку на функцию dispatch из Redux Store.

    Разбор процесса

  • Пользователь нажимает кнопку.
  • Срабатывает onClick.
  • Вызывается increment(). Это функция из слайса (Action Creator), которая возвращает объект вида { type: 'counter/increment', payload: undefined }.
  • Этот объект передается в функцию dispatch.
  • Redux Store принимает действие, прогоняет его через редьюсеры, обновляет состояние и уведомляет всех подписчиков (все компоненты с useSelector).
  • !Цикл обновления данных: от клика в компоненте до перерисовки интерфейса

    Практический пример: Список задач

    Давайте объединим полученные знания и создадим компонент для списка задач. Предположим, у нас уже есть слайс todosSlice.

    Файл features/todos/TodoList.jsx:

    Анализ примера

    В этом примере мы видим гармоничное сочетание локального состояния React (useState для текста инпута) и глобального состояния Redux (список задач).

    * Локальное состояние: Текст, который пользователь набирает прямо сейчас, не нужен другим частям приложения. Хранить его в Redux было бы избыточно, так как это вызывало бы глобальные обновления при каждом нажатии клавиши. Поэтому мы используем useState. * Глобальное состояние: Список задач (todos) важен для приложения, его нужно сохранять и, возможно, показывать на других экранах. Поэтому мы читаем его через useSelector и меняем через dispatch.

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

    При интеграции React и Redux новички часто совершают несколько типичных ошибок:

  • Забытый Provider: Если вы попытаетесь использовать useSelector без оборачивания приложения в <Provider>, вы получите ошибку, так как хук не сможет найти контекст Redux.
  • Мутация в компонентах: Никогда не пытайтесь изменить данные, полученные из useSelector, напрямую (например, todos[0].text = 'New Text'). Данные в Redux доступны только для чтения. Изменения — только через dispatch.
  • Вызов dispatch при рендеринге: Не вызывайте dispatch прямо в теле компонента (вне useEffect или обработчиков событий). Это приведет к бесконечному циклу перерисовок.
  • Заключение

    Мы успешно интегрировали Redux в React-приложение. Теперь вы знаете, что:

    * <Provider> — это компонент-обертка, который делает Store доступным. * useSelector — это хук для чтения данных и подписки на обновления. * useDispatch — это хук для отправки команд на изменение состояния.

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

    4. Асинхронная логика: Работа с побочными эффектами и API с помощью createAsyncThunk

    Асинхронная логика: Работа с побочными эффектами и API с помощью createAsyncThunk

    До этого момента мы работали с Redux в идеальном синхронном мире. Мы нажимали кнопку, действие мгновенно попадало в редьюсер, состояние обновлялось, и интерфейс перерисовывался. Весь этот процесс занимал миллисекунды.

    Но реальный веб устроен иначе. Данные не живут внутри приложения изначально — они приходят с сервера. Запросы к API занимают время, могут завершиться ошибкой или вернуть неожиданный результат. В этой статье мы узнаем, как научить Redux работать с асинхронностью, не нарушая его главный принцип — чистоту функций.

    Проблема чистых функций и побочные эффекты

    Вспомним золотое правило Redux: Редьюсеры должны быть чистыми функциями. Это означает, что внутри редьюсера:

  • Нельзя делать HTTP-запросы (fetch, axios).
  • Нельзя использовать случайные значения (Math.random).
  • Нельзя вызывать функции с побочными эффектами.
  • Редьюсер должен просто взять старое состояние и действие, а затем вернуть новое состояние. Если мы попытаемся вставить fetch внутрь редьюсера, мы сломаем предсказуемость приложения, инструменты разработчика и машину времени (Time Travel Debugging).

    Где же тогда должна жить логика общения с сервером?

    Ответ: в Middleware (Промежуточном ПО) и специально подготовленных функциях, которые называются Thunks (Санки).

    Что такое Thunk?

    Термин «Thunk» пришел из программирования и означает функцию, которая откладывает вычисление выражения. В контексте Redux, Thunk — это функция, которая возвращает другую функцию, принимающую dispatch и getState.

    Это позволяет нам писать логику, которая взаимодействует с хранилищем асинхронно. Мы можем отправить действие «Начало загрузки», сделать запрос, подождать ответ, и затем отправить действие «Загрузка завершена» или «Ошибка».

    К счастью, Redux Toolkit уже включает в себя middleware redux-thunk по умолчанию при использовании configureStore. Нам не нужно ничего настраивать вручную.

    createAsyncThunk: Современный подход

    В старом Redux нам приходилось вручную создавать три типа действий для каждого запроса:

    * FETCH_USERS_REQUEST (начало загрузки) * FETCH_USERS_SUCCESS (успех) * FETCH_USERS_FAILURE (ошибка)

    И писать генератор действия (thunk), который последовательно их отправляет. Redux Toolkit упрощает это с помощью функции createAsyncThunk.

    Эта функция принимает два аргумента:

  • Строковый тип действия: Префикс для генерации типов действий (например, 'users/fetchById').
  • Payload Creator: Асинхронная функция, которая возвращает промис (делает запрос).
  • Создание асинхронного действия

    Давайте создадим thunk для загрузки списка пользователей с бесплатного API JSONPlaceholder.

    Когда вы вызываете dispatch(fetchUsers()), createAsyncThunk автоматически генерирует и отправляет три жизненных цикла действия:

  • users/fetchUsers/pending: Отправляется перед началом выполнения payload creator.
  • users/fetchUsers/fulfilled: Отправляется, если промис успешно разрешился. В action.payload будет то, что мы вернули из функции.
  • users/fetchUsers/rejected: Отправляется, если промис был отклонен (ошибка сети или исключение).
  • !Визуализация потока данных при использовании createAsyncThunk: от вызова в компоненте до обновления Store через три стадии: pending, fulfilled и rejected.

    Обработка действий в слайсе: extraReducers

    Теперь, когда у нас есть thunk, нам нужно научить наш слайс реагировать на эти три типа действий.

    Обычное поле reducers в createSlice создает действия и редьюсеры одновременно. Но createAsyncThunk создает действия вне слайса. Чтобы слайс мог слушать «чужие» или внешние действия, используется поле extraReducers.

    Давайте создадим полноценный слайс для пользователей.

    Разбор extraReducers

    Мы используем функцию обратного вызова (callback), которая принимает объект builder. Это рекомендуемый способ написания extraReducers в современном Redux Toolkit (так как он лучше работает с TypeScript).

    * builder.addCase(actionCreator, reducer): Говорит слайсу: «Если прилетит действие такого типа, выполни этот код». * Мы обращаемся к свойствам thunk-а: fetchUsers.pending, fetchUsers.fulfilled и fetchUsers.rejected, чтобы получить нужные типы действий.

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

    Теперь подключим это к React. Нам понадобятся хуки useDispatch для запуска загрузки и useSelector для отображения состояния.

    Обратите внимание на проверку if (status === 'idle'). Это распространенный паттерн, предотвращающий повторную загрузку данных, если они уже были загружены или находятся в процессе загрузки.

    Обработка ошибок

    Что произойдет, если сервер вернет ошибку 500 или пропадет интернет? Промис в fetch (если это сетевая ошибка) отклонится, и сработает rejected.

    Однако, важно помнить особенность fetch: он не отклоняет промис при HTTP-ошибках (404, 500). Он отклоняет его только при сбое сети. Чтобы createAsyncThunk корректно перешел в состояние rejected при ошибке 404, нужно выбросить ошибку вручную внутри payload creator:

    В этом примере мы используем второй аргумент payload creator-а — thunkAPI. Из него мы деструктурируем функцию rejectWithValue. Это позволяет нам вернуть конкретное сообщение об ошибке, которое будет доступно в редьюсере через action.payload, а не через action.error.

    Передача параметров

    Часто нам нужно передать параметры в запрос, например, ID пользователя или поисковый запрос. createAsyncThunk принимает аргумент, который передается первым параметром в payload creator.

    Если нужно передать несколько параметров, передавайте их как один объект:

    Заключение

    Мы разобрали, как Redux Toolkit решает проблему асинхронности. createAsyncThunk берет на себя всю рутинную работу по созданию типов действий и управлению жизненным циклом запроса. Нам остается только описать логику запроса и реакцию на изменение состояния в extraReducers.

    Основные выводы:

  • Редьюсеры должны оставаться чистыми. Асинхронность выносится в thunks.
  • createAsyncThunk генерирует три действия: pending, fulfilled, rejected.
  • Для обработки этих действий в слайсе используется поле extraReducers.
  • Состояние загрузки (loading/error/success) обычно хранится в Store вместе с данными.
  • В следующей статье мы рассмотрим продвинутые техники оптимизации производительности в Redux и использование селекторов.

    5. Продвинутые техники: Оптимизация производительности с Reselect и отладка через Redux DevTools

    Продвинутые техники: Оптимизация производительности с Reselect и отладка через Redux DevTools

    Поздравляю! Вы прошли долгий путь от понимания архитектуры Flux до написания асинхронных thunk-функций. На данном этапе у вас есть работающее приложение, которое умеет загружать данные с сервера и управлять сложным состоянием. Но в мире профессиональной разработки «работает» — это только половина дела. Вторая половина — это «работает быстро» и «легко поддерживается».

    В этой статье мы сосредоточимся на двух критически важных аспектах разработки: оптимизации производительности с помощью мемоизации (библиотека Reselect) и профессиональной отладке с использованием Redux DevTools.

    Проблема производительности в React-Redux

    Чтобы понять, зачем нам нужна оптимизация, давайте вспомним, как работает хук useSelector. Каждый раз, когда в Redux Store происходит какое-либо действие (action), useSelector запускается заново. Он берет текущее состояние, выполняет вашу функцию-селектор и сравнивает результат с предыдущим.

    Если результат отличается, компонент React перерисовывается (re-render).

    Проблема ссылочной целостности

    В JavaScript сравнение объектов и массивов происходит по ссылке, а не по значению. Рассмотрим простой пример селектора, который фильтрует список задач:

    Метод .filter() всегда возвращает новый массив, даже если элементы внутри не изменились. Это означает, что при любом действии в приложении (даже если оно не связано с задачами, например, обновление профиля пользователя), этот селектор вернет новую ссылку. React увидит, что ссылка изменилась, и перерисует компонент.

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

    Решение: Мемоизация и Reselect

    Для решения этой проблемы используется техника, называемая мемоизацией. Это способ оптимизации, при котором результат выполнения функции сохраняется (кэшируется). Если функцию вызывают повторно с теми же аргументами, она не выполняет вычисления заново, а просто возвращает сохраненный результат.

    Математически это можно представить так:

    Где — результат функции, — функция вычисления, а и — входные аргументы.

    Если и не изменились с прошлого раза, мы мгновенно возвращаем старый , пропуская работу функции .

    Библиотека Reselect

    В экосистеме Redux стандартом для создания мемоизированных селекторов является библиотека Reselect. Хорошая новость заключается в том, что вам не нужно её устанавливать отдельно — она уже встроена в Redux Toolkit и экспортируется как функция createSelector.

    createSelector позволяет создавать так называемые «умные» селекторы. Они состоят из двух частей:

  • Входные селекторы (Input Selectors): Извлекают нужные куски данных из state. Обычно это простые функции.
  • Функция преобразования (Result Function): Принимает результаты входных селекторов и выполняет сложные вычисления (фильтрацию, сортировку, маппинг).
  • !Диаграмма потока данных в Reselect: как входные данные проходят проверку на изменения перед вычислением результата

    Практический пример

    Давайте перепишем наш проблемный селектор с использованием createSelector.

    Теперь, если мы используем selectFilteredTodos в компоненте:

    Если в Store изменится что-то постороннее (например, имя пользователя), selectAllTodos и selectFilterStatus вернут те же самые объекты. createSelector увидит это, не станет запускать функцию фильтрации и вернет старый массив. Компонент не перерисуется. Профит!

    Redux DevTools: Рентген для вашего приложения

    Вторая часть нашей статьи посвящена отладке. Вы уже настроили configureStore, который автоматически включает поддержку Redux DevTools Extension. Это расширение для браузера, которое превращает разработку в удовольствие.

    Если вы еще не установили его, сделайте это сейчас через магазин расширений вашего браузера (Chrome, Firefox, Edge).

    Основные возможности

    Когда вы открываете вкладку Redux в инструментах разработчика, вы видите интерфейс, разделенный на две основные части: левая панель со списком действий и правая панель с деталями.

    !Интерфейс Redux DevTools с историей действий и инспектором состояния

    #### 1. Инспектор действий (Action Inspector)

    Каждое действие, которое проходит через dispatch, записывается в левой колонке. Нажав на любое действие, вы можете изучить его детали справа:

    * Diff: Показывает разницу между состоянием ДО и ПОСЛЕ действия. Зеленым подсвечивается то, что добавилось, красным — то, что удалилось. Это самый полезный вид для понимания того, что именно изменил ваш редьюсер. * Action: Показывает сам объект действия (type и payload). Полезно, чтобы проверить, правильные ли данные пришли с сервера или из формы. * State: Показывает полное дерево состояния приложения на момент после выполнения этого действия.

    #### 2. Путешествие во времени (Time Travel Debugging)

    Это «киллер-фича» Redux. Внизу панели есть слайдер (ползунок). Передвигая его, вы можете буквально отматывать время назад.

    Как это работает? Redux хранит историю всех действий. Когда вы двигаете ползунок назад, DevTools берет начальное состояние и последовательно применяет действия до нужной точки. Ваш интерфейс в браузере будет обновляться в реальном времени, показывая, как выглядело приложение 5 минут назад.

    Это незаменимо для отладки сложных багов. Вы можете:

  • Воспроизвести баг.
  • Отмотать время назад, чтобы увидеть момент перед ошибкой.
  • Понять, какое действие привело к некорректному состоянию.
  • #### 3. Dispatching (Ручная отправка действий)

    В DevTools есть кнопка «Dispatcher» (обычно иконка клавиатуры). Она позволяет вам вручную написать и отправить действие в Store, не нажимая кнопок в интерфейсе приложения.

    Пример:

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

    Лучшие практики оптимизации

    Подводя итог теме производительности, вот чек-лист для вашего Redux-приложения:

  • Не оптимизируйте преждевременно. Redux достаточно быстр сам по себе. Внедряйте createSelector только там, где вы преобразуете данные (фильтрация, сортировка, сложные вычисления) или создаете новые объекты/массивы.
  • Держите State плоским. Чем меньше вложенность данных, тем проще писать селекторы и обновлять состояние.
  • Используйте React.memo. Если компонент получает данные из Redux через пропсы, оберните его в React.memo, чтобы избежать перерисовок, когда пропсы не меняются.
  • Заключение

    Сегодня мы превратили наше приложение из просто «рабочего» в «профессиональное». Мы научились экономить ресурсы браузера с помощью мемоизации через createSelector и получили полный контроль над потоком данных благодаря Redux DevTools.

    Теперь вы обладаете полным арсеналом инструментов для создания масштабируемых приложений: от настройки Store и слайсов до асинхронных запросов и тонкой оптимизации. На этом наш курс «Освоение Redux» подходит к концу. Эти знания станут прочным фундаментом для вашей карьеры React-разработчика. Удачи в создании крутых проектов!