Redux Toolkit и RTK Query: От основ до PRO на TypeScript

Практический курс по современному управлению состоянием в React с использованием TypeScript и Tailwind CSS. Вы пройдете путь от настройки Store до реализации сложной логики кэширования и мутаций данных с помощью RTK Query.

1. Настройка Redux Store, типизация хуков и интеграция с React на TypeScript

Настройка Redux Store, типизация хуков и интеграция с React на TypeScript

Добро пожаловать в курс Redux Toolkit и RTK Query: От основ до PRO на TypeScript. Это первая статья нашего цикла, в которой мы заложим фундамент для всех будущих приложений. Мы не просто настроим Redux, мы сделаем это правильно: с полной типизацией, использованием современных хуков и интеграцией в React-приложение.

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

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

Прежде чем писать код, нам нужно установить необходимые пакеты. Мы предполагаем, что у вас уже развернут React-проект на TypeScript (например, через Vite или Create React App).

Выполните следующую команду в терминале:

Здесь: * @reduxjs/toolkit — основная библиотека, включающая методы для создания слайсов, стора и RTK Query. * react-redux — библиотека для связывания Redux с компонентами React.

Архитектура Redux Toolkit

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

!Архитектура взаимодействия React компонентов и Redux Store через Слайсы

Шаг 1: Создание Слайса (Slice)

Создадим простую функциональность счетчика, чтобы продемонстрировать типизацию. Создайте файл src/store/features/counter/counterSlice.ts.

Обратите внимание на PayloadAction<number>. Это дженерик-тип, который гарантирует, что в action.payload придет именно число, а не строка или объект. Это и есть сила TypeScript.

Шаг 2: Настройка Store

Теперь создадим само хранилище. Создайте файл src/store/store.ts.

Функция configureStore автоматически настраивает инструменты разработчика (Redux DevTools) и добавляет полезные middleware (например, для проверки на случайные мутации).

Ключевой момент здесь — экспорт типов RootState и AppDispatch. Мы не пишем их вручную, мы просим TypeScript вывести их на основе созданного объекта store. Это гарантирует, что типы всегда будут актуальны при изменении редюсеров.

Шаг 3: Типизация хуков

В обычном JavaScript мы бы использовали useDispatch и useSelector напрямую из библиотеки react-redux. Однако в TypeScript это неудобно: useSelector не знает структуру нашего стейта, а useDispatch не знает о thunk-экшенах.

Лучшая практика — создать типизированные версии этих хуков. Создайте файл src/store/hooks.ts.

Теперь, когда вы будете использовать useAppSelector, TypeScript будет подсказывать вам поля вашего состояния (например, state.counter.value).

Шаг 4: Провайдер Store

Чтобы React увидел Redux, нужно обернуть приложение в компонент Provider. Обычно это делается в корневом файле, например main.tsx или index.tsx.

Интеграция в компонент

Теперь соберем всё вместе. Создадим компонент, который использует наши типизированные хуки и стилизован с помощью Tailwind CSS.

Разбор кода компонента

  • useAppSelector: Мы передаем функцию (state) => state.counter.value. Благодаря типизации, IDE знает, что у state есть поле counter, а у него — поле value. Если вы ошибетесь в названии, TypeScript немедленно подсветит ошибку.
  • useAppDispatch: Возвращает функцию dispatch, которая принимает только валидные экшены. Если вы попробуете отправить incrementByAmount('пять') (строку вместо числа), TypeScript укажет на несоответствие типа PayloadAction<number>.
  • Почему это важно?

    Использование Redux Toolkit в связке с TypeScript дает нам:

    * Безопасность: Вы не можете обратиться к несуществующему полю стейта. * Скорость разработки: Автодополнение кода работает великолепно. * Читаемость: Структура кода становится стандартизированной и понятной любому разработчику, знакомому с RTK.

    В следующей статье мы углубимся в работу с асинхронными данными и рассмотрим, как RTK Query меняет правила игры при работе с API.

    2. Архитектура данных: создание слайсов, редьюсеров и экшенов в Redux Toolkit

    Архитектура данных: создание слайсов, редьюсеров и экшенов в Redux Toolkit

    В предыдущей статье мы настроили базовое хранилище (Store) и типизировали хуки. Теперь пришло время углубиться в сердце Redux Toolkit — функцию createSlice. Именно здесь происходит магия, превращающая рутинное написание кода в элегантный и поддерживаемый процесс.

    Мы перейдем от простого счетчика к более сложной структуре данных — списку задач (Todo List). Это позволит нам разобрать работу с массивами, объектами, подготовку данных (prepare callback) и принцип иммутабельности.

    Анатомия Слайса (Slice)

    В классическом Redux нам приходилось создавать три разных сущности: константы типов экшенов (ACTION_TYPE), генераторы экшенов (action creators) и функцию-редьюсер. Это приводило к размазыванию логики по разным файлам.

    Слайс в Redux Toolkit объединяет эти концепции. Вы описываете состояние и то, как оно меняется, а библиотека сама генерирует остальное.

    !Схема показывает, как createSlice принимает объект конфигурации и автоматически генерирует редьюсер и экшены.

    Проектирование состояния

    Начнем с создания нового файла src/store/features/todos/todosSlice.ts. Первым делом определим типы.

    Здесь мы используем nanoid — встроенную в RTK утилиту для генерации уникальных строковых ID. Она легче и быстрее, чем UUID.

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

    Одно из главных правил Redux: состояние должно быть неизменяемым (immutable). В классическом Redux для добавления элемента в массив нам приходилось писать:

    Redux Toolkit использует библиотеку Immer. Она оборачивает состояние в Proxy-объект. Вы можете писать код так, будто вы изменяете состояние напрямую (мутируете его), а Immer отследит изменения и создаст новую копию состояния безопасным способом.

    Создадим слайс с базовыми действиями:

    Обратите внимание на toggleTodo. Мы нашли объект и просто изменили его свойство completed. Без RTK это было бы грубой ошибкой, но здесь это абсолютно легально и правильно.

    Продвинутая техника: prepare callback

    Частая проблема новичков: где генерировать уникальный ID или текущую дату при создании новой задачи? В компоненте или в редьюсере?

  • В редьюсере нельзя: Редьюсеры должны быть чистыми функциями. nanoid() или Date.now() выдают разные значения при каждом вызове, что нарушает чистоту.
  • В компоненте нежелательно: Компонент должен отвечать за UI, а не за логику формирования данных.
  • Решение от RTK — колбэк prepare. Он позволяет трансформировать аргументы, переданные в экшен, до того, как они попадут в редьюсер.

    Добавим экшен addTodo в наш слайс:

    Теперь, когда мы будем вызывать dispatch(addTodo('Купить молоко')) в компоненте, RTK автоматически вызовет prepare, сгенерирует ID и сформирует полный объект Todo.

    Полный код слайса

    Соберем всё вместе и экспортируем необходимые части.

    Не забудьте подключить этот редьюсер в store.ts (который мы создали в прошлом уроке):

    Создание UI компонента

    Теперь создадим компонент TodoList, который будет использовать наш новый слайс. Мы будем использовать Tailwind CSS для стилизации.

    Создайте файл src/components/TodoList.tsx.

    Разбор ключевых моментов

  • Типизация useAppSelector: Когда мы пишем state.todos.list, TypeScript точно знает, что это массив объектов Todo. Если мы попробуем обратиться к todo.title вместо todo.text, компилятор выдаст ошибку.
  • Диспатч экшенов: Мы вызываем dispatch(toggleTodo(todo.id)). Функция toggleTodo была сгенерирована автоматически из имени слайса (todos) и имени редьюсера (toggleTodo), создавая тип экшена todos/toggleTodo.
  • Резюме

    В этой статье мы разобрали архитектуру данных в Redux Toolkit:

    * Слайсы (Slices) заменяют собой разрозненные файлы экшенов и редьюсеров. * Immer позволяет писать логику изменения состояния так, будто мы мутируем объекты, сохраняя при этом иммутабельность. * prepare callback — мощный инструмент для выноса "грязной" логики (генерация ID) из компонентов и сохранения чистоты редьюсеров.

    Теперь у нас есть полностью рабочее приложение с синхронной логикой. Но что делать, если задачи нужно загружать с сервера? В следующей статье мы разберем асинхронные операции и createAsyncThunk.

    3. Взаимодействие UI и Store: выборка данных и диспетчеризация событий в компонентах

    Взаимодействие UI и Store: выборка данных и диспетчеризация событий в компонентах

    Мы уже проделали большую работу: настроили Redux Store, типизировали хуки и создали слайс для списка задач. У нас есть «двигатель» (Store) и «топливо» (данные и логика в слайсе), но машина не поедет, пока мы не соединим их с «рулем» и «приборной панелью» — нашим пользовательским интерфейсом (UI).

    В этой статье мы научимся правильно извлекать данные из хранилища, отправлять команды на изменение состояния и реализуем логику фильтрации задач. Мы будем использовать наши кастомные хуки useAppSelector и useAppDispatch, созданные в первом уроке.

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

    Прежде чем писать код, важно визуализировать, как данные перемещаются в приложении. Redux использует однонаправленный поток данных (Unidirectional Data Flow).

    !Циклический процесс обновления состояния: UI вызывает Action, Reducer обновляет Store, Selector обновляет UI.

  • View (UI): Компонент отображает данные.
  • Dispatch: Пользователь совершает действие (клик), компонент отправляет (dispatch) экшен.
  • Reducer: Слайс перехватывает экшен и обновляет состояние.
  • Selector: Компонент замечает изменение данных через селектор и перерисовывается.
  • Чтение данных: useAppSelector

    Для получения данных из Redux используется хук-селектор. В нашем случае это типизированный useAppSelector.

    Что такое селектор?

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

    Вы также можете писать селектор инлайн (прямо внутри хука), что часто удобнее для простых операций:

    Правила рендеринга

    useAppSelector — это умный хук. Он автоматически подписывается на обновления Redux Store. Однако компонент будет перерисован (re-render) только тогда, когда значение, возвращаемое селектором, изменится.

    Сравнение происходит по ссылке (===).

    * Если селектор возвращает примитив (число, строку, булево значение) — ререндер будет только при смене значения. * Если селектор возвращает объект или массив — ререндер будет, если изменилась ссылка на этот объект.

    > Важно: Старайтесь выбирать из стейта минимально необходимые данные. Если вы выберете весь state.todos, компонент будет перерисовываться при изменении любого поля в этом слайсе, даже если оно не используется в данном компоненте.

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

    Чтобы изменить состояние, мы не можем просто написать state.value = 10 внутри компонента. Мы обязаны отправить «сигнал» или «событие» — Action.

    Для этого используется хук useAppDispatch.

    Практика: Реализация фильтрации задач

    Давайте усложним наше приложение из прошлого урока. Добавим возможность фильтровать задачи: «Все», «Активные» и «Завершенные».

    Шаг 1: Обновление Слайса

    Нам нужно где-то хранить текущий выбранный фильтр. Добавим его в todosSlice.ts.

    Откройте src/store/features/todos/todosSlice.ts и внесите изменения:

    Шаг 2: Компонент фильтрации

    Создадим небольшой компонент TodoFilters для кнопок переключения. Он будет читать текущий фильтр и менять его.

    Шаг 3: Вычисляемые данные (Derived Data)

    Теперь самое интересное. В компоненте списка нам нужно отображать не все задачи, а только те, которые соответствуют фильтру.

    Где производить фильтрацию? В редьюсере? Нет. В компоненте? Да.

    Лучшая практика Redux: храните в Store минимальный набор данных, а всё остальное вычисляйте в компонентах (или селекторах) на лету.

    Обновим наш основной компонент TodoList.tsx:

    Локальный стейт vs Глобальный стейт

    Частый вопрос новичков: «Нужно ли мне хранить значение инпута (text) в Redux?»

    Ответ: Нет.

    Используйте правило: * Глобальный стейт (Redux): Данные, которые нужны многим компонентам, или данные приложения (список задач, данные пользователя, настройки темы). * Локальный стейт (useState): Данные, которые касаются только текущего компонента UI (значение поля ввода, открыт ли выпадающий список, активная вкладка).

    В нашем примере text (текст новой задачи) нужен только компоненту TodoList в момент набора. Как только задача добавлена в Redux, локальный стейт очищается. Это идеальное разделение ответственности.

    Резюме

    В этой статье мы замкнули круг Redux:

  • Мы научились читать данные с помощью useAppSelector.
  • Мы научились изменять данные, отправляя экшены через useAppDispatch.
  • Мы реализовали логику Derived Data (вычисляемых данных), отфильтровав список задач на основе состояния.
  • Теперь наше приложение интерактивно. Но что, если данные нужно сохранить на сервере? В следующей статье мы перейдем к одной из самых мощных возможностей Redux Toolkit — асинхронным Thunks и работе с API.

    4. Основы RTK Query: создание API, выполнение запросов и автоматическое кэширование

    Основы RTK Query: создание API, выполнение запросов и автоматическое кэширование

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

    Раньше для этого приходилось писать много шаблонного кода: создавать thunks, вручную отслеживать состояния isLoading и isError, и самое сложное — самостоятельно управлять кэшированием данных.

    В этой статье мы познакомимся с RTK Query — мощным инструментом, встроенным в Redux Toolkit, который берет на себя всю грязную работу по выборке и кэшированию данных.

    Что такое RTK Query и зачем он нужен?

    RTK Query — это надстройка над Redux Toolkit, предназначенная для упрощения работы с API. Она решает следующие задачи:

  • Отслеживание состояния загрузки: Вам больше не нужно создавать отдельные флаги loading в редьюсерах.
  • Кэширование: Если данные уже загружены, RTK Query не будет дергать сервер лишний раз.
  • Дедупликация запросов: Если два компонента одновременно запрашивают одни и те же данные, на сервер уйдет только один запрос.
  • Автоматическое обновление: Данные в UI обновляются автоматически при изменении кэша.
  • !Схема работы кэширования в RTK Query: компонент обращается к кэшу, а кэш управляет запросами к серверу.

    Шаг 1: Создание API сервиса

    В отличие от обычных слайсов, API в RTK Query определяется в одном месте. Мы создадим новый файл для работы с нашим бэкендом (в учебных целях будем использовать JSONPlaceholder или имитировать его).

    Создайте файл src/store/services/todosApi.ts.

    Разбор кода

    * createApi: Основная функция. Она создает слайс и набор мидлваров. * fetchBaseQuery: Это легкая обертка над стандартным fetch. Она упрощает настройку заголовков и базового URL. * endpoints: Здесь мы перечисляем все операции (получение данных — query, изменение данных — mutation). * builder.query<ResultType, QueryArg>: Это TypeScript-дженерик. Он критически важен для типизации. Если вы укажете здесь правильные типы, то в компоненте data будет иметь именно этот тип.

    Шаг 2: Подключение к Redux Store

    Созданный API нужно зарегистрировать в нашем store. Это делается немного иначе, чем с обычными слайсами.

    Откройте файл src/store/store.ts и внесите изменения:

    Зачем нужен Middleware?

    Добавление todosApi.middleware обязательно. Именно этот код отвечает за: * Управление временем жизни кэша. * Опросы сервера (polling). * Инвалидацию кэша (автоматическое обновление данных после мутаций).

    Шаг 3: Использование хука в компоненте

    Теперь самое приятное. Нам не нужно писать useEffect, useState для загрузки и ошибок. Всё это уже есть в сгенерированном хуке useGetTodosQuery.

    Давайте создадим новый компонент ServerTodoList.tsx, чтобы не ломать наш старый локальный список, и посмотрим, как это работает.

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

    Магия автоматического кэширования

    Одна из главных фишек RTK Query — это умное кэширование. Давайте разберем, как это работает на практике.

    Представьте, что у вас есть два компонента на странице, и оба используют useGetTodosQuery().

  • Первый компонент монтируется. RTK Query видит, что данных в кэше нет. Он отправляет запрос на сервер. Статус меняется на isLoading: true.
  • Второй компонент монтируется сразу же после первого. RTK Query видит, что запрос на эти данные уже «в полете» (pending). Он не отправляет второй запрос.
  • Когда данные приходят, оба компонента обновляются одновременно.
  • Если вы перейдете на другую страницу и вернетесь назад, RTK Query мгновенно отдаст данные из кэша, не показывая спиннер загрузки (если время жизни кэша не истекло).

    !Временная диаграмма, показывающая дедупликацию запросов: второй компонент использует результат первого запроса, не создавая новый.

    Управление временем жизни кэша

    По умолчанию RTK Query хранит неиспользуемые данные в кэше 60 секунд. Если все компоненты, использующие эти данные, размонтируются, запустится таймер. Если в течение 60 секунд никто снова не запросит эти данные, они будут удалены из памяти.

    Вы можете настроить это поведение через параметр keepUnusedDataFor:

    Типизация аргументов запроса

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

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

    Если аргумент изменится (например, вы передадите '2'), RTK Query автоматически сделает новый запрос, так как это уже другой ключ кэша.

    Резюме

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

    Мы узнали:

  • Как создавать API-сервисы с помощью createApi и fetchBaseQuery.
  • Как правильно подключать API к Redux Store, не забывая про middleware.
  • Как использовать автогенерируемые хуки (useGetTodosQuery) для получения данных.
  • Как работает дедупликация и кэширование запросов.
  • Теперь наше приложение умеет читать данные с сервера. Но полноценное приложение должно уметь и изменять их. В следующей статье мы разберем Мутации (Mutations) — способ отправлять POST, PUT и DELETE запросы, и научимся автоматически обновлять списки после изменений.

    5. Продвинутый RTK Query: мутации данных, инвалидация тегов и оптимистичные обновления

    Продвинутый RTK Query: мутации данных, инвалидация тегов и оптимистичные обновления

    Добро пожаловать обратно! В предыдущей статье мы научились «читать» данные с сервера, используя builder.query. Наше приложение теперь умеет отображать список задач, загруженный из API, и даже кэшировать его, чтобы не нагружать сеть лишними запросами.

    Но приложение, которое умеет только читать, — это просто цифровая газета. Настоящее веб-приложение должно уметь создавать, обновлять и удалять данные. В терминологии RTK Query эти операции называются Мутациями (Mutations).

    Сегодня мы превратим наш список задач в полноценный инструмент управления делами. Мы разберем:

  • Как создавать и типизировать мутации.
  • Почему данные не обновляются сами по себе и как это исправить с помощью Тегов.
  • Как реализовать Оптимистичные обновления (Optimistic Updates), чтобы интерфейс реагировал мгновенно, не дожидаясь ответа сервера.
  • Анатомия Мутации

    В отличие от запросов (query), которые служат для получения данных, мутации (mutation) используются для отправки обновлений на сервер и применения изменений в локальном кэше. Это включает в себя методы HTTP: POST, PUT, PATCH и DELETE.

    Вернемся к нашему файлу src/store/services/todosApi.ts и добавим возможность создания новой задачи.

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

  • getTodos загружает данные и помечает их тегом { type: 'Todo', id: 'LIST' }.
  • Мы вызываем addTodo.
  • После успеха addTodo срабатывает invalidatesTags: [{ type: 'Todo', id: 'LIST' }].
  • RTK Query видит, что тег LIST был инвалидирован.
  • Он ищет все активные запросы, которые зависят от этого тега (getTodos).
  • Он автоматически перезапускает getTodos.
  • Список обновляется, и пользователь видит новую задачу.
  • Оптимистичные обновления (Optimistic Updates)

    Инвалидация тегов — это надежно, но иногда медленно. Пользователь нажимает «Удалить», ждет 0.5 секунды ответа сервера, потом еще 0.5 секунды перезагрузки списка. В современном вебе мы хотим мгновенной реакции.

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

    В RTK Query это делается через onQueryStarted внутри мутации.

    Давайте реализуем оптимистичное удаление задачи:

    Разбор магии

  • todosApi.util.updateQueryData: Это утилита для ручного изменения кэша. Она принимает имя эндпоинта (getTodos), аргументы кэша (у нас undefined или void) и функцию-рецепт.
  • draft: Благодаря Immer мы работаем с черновиком состояния. Мы просто находим индекс и удаляем элемент через splice.
  • patchResult.undo(): Если queryFulfilled выбросит ошибку (сервер упал), вызов undo() вернет кэш в состояние до нашего вмешательства. Задача «вернется» в список.
  • Теперь, когда вы нажмете «Удалить», задача исчезнет мгновенно. Пользовательский опыт (UX) становится невероятно плавным.

    Резюме

    Сегодня мы превратили наше приложение из «читателя» в полноценного «редактора» данных.

    Мы изучили: * Мутации (builder.mutation): Способ отправлять изменения на сервер. * Теги (Tags): Механизм автоматической синхронизации данных. providesTags вешает метки, invalidatesTags срывает их, вызывая перезагрузку. * Оптимистичные обновления: Высший пилотаж UX, позволяющий интерфейсу работать быстрее, чем отвечает сервер.

    В следующем уроке мы рассмотрим продвинутые техники: обработку ошибок, middleware и трансформацию ответов, чтобы сделать наше приложение пуленепробиваемым.