Мы продолжаем изучение управления состоянием с Redux и Redux Toolkit. До этого мы работали с синхронными операциями. Теми, которые выполняются мгновенно, «Лайк» увеличение счетчика или добавление задачи в список. Но современные веб-приложения живут данными, которые приходят с серверов. Это асинхронные операции, запросы к API, работа с базой данных, загрузка файлов. Они не происходят мгновенно и их результат неизвестен заранее. Он может быть успешным, а может и провалиться. Сегодня мы научимся надежно работать с такими операциями в Redux Toolkit с помощью мощной функции createAsyncThunk.
Проблема асинхронности в Redux
Давай на секунду представим, как бы мы пытались сделать запрос к API в «ванильном» Redux, без специальных инструментов. Допустим, нам нужно загрузить список пользователей.
-
Диспатч экшена начала загрузки:
{ type: 'users/fetchUsers/pending' }. Это нужно, чтобы наш интерфейс отреагировал, показал, например спиннер загрузки. -
Выполнение самого запроса: Мы бы использовали
fetchилиaxiosвнутри действия (action creator), которое не является чистой функцией. -
Обработка результата:
-
Если запрос успешен, мы диспатчим экшен успеха:
{ type: 'users/fetchUsers/fulfilled', payload: data }. -
Если произошла ошибка, мы диспатчим экшен ошибки:
{ type: 'users/fetchUsers/rejected', error: 'Something went wrong' }.
-
Проблема в том, что «голый» Redux ожидает, что экшены это простые объекты, а редьюсеры чистые функции. Запрос к API это side effect (побочный эффект), который не является ни чистым, ни синхронным. Раньше для решения этой проблемы приходилось использовать промежуточное ПО (middleware), самое популярное из которых, это redux-thunk. Thunk позволяет диспатчить не только объекты, но и функции, которые могут содержать side effects и сами диспатчить экшены.
Redux Toolkit включает redux-thunk «из коробки» и предоставляет нам абстракцию более высокого уровня createAsyncThunk. Эта функция автоматически генерирует три типа экшенов (pending, fulfilled, rejected) и берет на всю грязную работу по их диспатчу. Нам остается только описать сам запрос и то, как состояние должно изменяться в каждом из этих трех сценариев.
Знакомство с createAsyncThunk
createAsyncThunk это утилита, которая создает thunk action creator. Звучит сложно? Давай разберемся на пальцах.
Thunk это функция, которая возвращает другую функцию. createAsyncThunk создает такую функцию-обертку для нашего асинхронного запроса. При вызове эта обертка автоматически диспатчит соответствующие экшены на разных этапах жизни нашего запроса.
Сигнатура функции выглядит так:
const asyncThunk = createAsyncThunk( // 1. Префикс типа экшена (String) 'users/fetchUsers', // 2. "Полезная нагрузка" (payload creator) - асинхронная функция async (arg, thunkAPI) => { // Здесь делаем асинхронный запрос (например, на API) const response = await fetch('https://jsonplaceholder.typicode.com/users'); // Возвращаемые данные станут payload экшена `fulfilled` return await response.json(); } );
Разберем аргументы:
-
Первый аргумент это строка, префикс для генерируемых типов экшенов. Для нашего примера
'users/fetchUsers'createAsyncThunkсгенерирует три типа экшенов:-
'users/fetchUsers/pending' -
'users/fetchUsers/fulfilled' -
'users/fetchUsers/rejected'
Нам не нужно создавать эти типы вручную!
-
-
Второй аргумент это «создатель полезной нагрузки» (payload creator). Это асинхронная функция, которая содержит всю логику side effect’а (например запрос к API).
-
Она получает два аргумента.
arg(любые данные, которые ты передашь thunk’у при вызове, например, ID пользователя) иthunkAPI(объект, содержащий методы какdispatch,getState,rejectWithValueи другие, полезные для более сложных сценариев). -
Если функция выполняется успешно и возвращает данные, они становятся
action.payloadв экшенеfulfilled. -
Если функция выбрасывает ошибку (например, упал запрос), эта ошибка становится
action.errorв экшенеrejected.
-
Самое главное тебе не нужно думать о том, когда и какие экшены диспатчить. createAsyncThunk делает это автоматически. Твоя задача описать запрос и обработать эти экшены в редьюсере.
Три жизненных цикла асинхронного запроса
Любой асинхронный запрос имеет три фундаментальных состояния и createAsyncThunk формализует их в виде трех экшенов. Давай пройдемся по каждому из них и подумаем, как мы обычно хотим обновлять наше состояние приложения в ответ на них.
-
pending(ожидание). Запрос отправлен, но ответ еще не получен. В этот момент мы обычно хотим:-
Установить флаг
isLoadingвtrue, чтобы показать индикатор загрузки (например, спиннер). -
Сбросить флаг
isError(если предыдущий запрос завершился ошибкой). -
Очистить данные (опционально, зависит от логики приложения).
-
-
fulfilled(выполнено). Запрос завершился успешно и мы получили данные от сервера. Это счастливый сценарий, в котором мы:-
Устанавливаем флаг
isLoadingобратно вfalse(скрываем спиннер). -
Записываем полученные данные (которые находятся в
action.payload) в состояние. -
Устанавливаем флаг
isErrorвfalse.
-
-
rejected(отклонено). Запрос завершился с ошибкой (сервер вернул 500, пропал интернет и т.д.). В этом случае мы:-
Устанавливаем
isLoadingвfalse(скрываем спиннер, ведь загрузка больше не идет). -
Сохраняем информацию об ошибке (обычно из
action.error.messageили с помощьюrejectWithValue) в состояние, чтобы показать ее пользователю. -
Устанавливаем флаг
isErrorвtrue.
-
Теперь ключевой вопрос. Как мы «ловим» эти автоматически сгенерированные экшены и обновляем состояние в ответ на них? Для этого в слайсах используется специальное поле extraReducers.
Обработка жизненных циклов в слайсе с помощью extraReducers
Помнишь, в нашем слайсе для счетчика мы использовали поле reducers? Оно создает «редьюсеры» для экшенов, которые «принадлежат» этому слайсу и генерируются createSlice автоматически.
extraReducers это такое же поле в createSlice, но оно предназначено для обработки экшенов, которые не были сгенерированы самим createSlice. Это идеальное место для того, чтобы реагировать на экшены от createAsyncThunk и других слайсов.
Мы можем описать extraReducers используя «builder callback» нотацию. Это современный и рекомендованный способ, который обеспечивает лучшую TypeScript-типизацию.
const usersSlice = createSlice({ name: 'users', initialState: { items: [], isLoading: false, isError: false, error: '', }, reducers: { // Здесь наши синхронные редьюсеры... }, extraReducers: (builder) => { // Будем добавлять обработчики через builder.addCase } });
Внутри extraReducers мы используем builder.addCase() чтобы «подписать» наш слайс на обработку конкретных типов экшенов. Давай напишем обработчики для всех трех состояний нашего thunk’а.
Создаем слайс для загрузки пользователей с JSONPlaceholder
Давай соберем все знания воедино и создадим полноценный слайс для загрузки и отображения списка пользователей. В качестве учебного API мы будем использовать сервис JSONPlaceholder, который предоставляет фейковые данные для тестирования.
Шаг 1: Создаем асинхронный thunk
Сначала создадим сам thunk для загрузки пользователей. Обычно его объявляют прямо в файле слайса, перед createSlice.
// features/users/usersSlice.js import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; // Создаем асинхронный thunk export const fetchUsers = createAsyncThunk( 'users/fetchUsers', async (_, thunkAPI) => { try { const response = await fetch('https://jsonplaceholder.typicode.com/users'); // fetch не выбрасывает ошибку для HTTP статусов 4xx/5xx, поэтому проверяем вручную if (!response.ok) { throw new Error('Failed to fetch users'); } const data = await response.json(); return data; // Это станет action.payload в fulfilled } catch (error) { // Можно использовать thunkAPI.rejectWithValue для передачи структурированных данных об ошибке return thunkAPI.rejectWithValue(error.message); } } );
Обрати внимание:
-
Первый аргумент
argнам не нужен, поэтому мы заменяем его на_. -
Мы используем
try/catchчтобы перехватить возможные ошибки сети или парсинга JSON. -
В случае ошибки, мы используем
thunkAPI.rejectWithValueчтобы вернуть кастомное значение, которое станетaction.payloadв экшенеrejected(это часто удобнее, чем стандартный объект ошибки).
Шаг 2: Создаем слайс с extraReducers
Теперь опишем начальное состояние и то, как оно будет меняться при диспатче наших асинхронных экшенов.
const usersSlice = createSlice({ name: 'users', initialState: { items: [], isLoading: false, isError: false, error: '', }, reducers: { // Можно добавить синхронные редьюсеры, например, для очистки ошибки clearError: (state) => { state.isError = false; state.error = ''; } }, extraReducers: (builder) => { builder // Обработка начала загрузки (pending) .addCase(fetchUsers.pending, (state) => { state.isLoading = true; state.isError = false; // Сбрасываем флаг ошибки при новом запросе state.error = ''; }) // Обработка успешной загрузки (fulfilled) .addCase(fetchUsers.fulfilled, (state, action) => { state.isLoading = false; state.isError = false; // Записываем массив пользователей, пришедший с сервера, в state.items state.items = action.payload; }) // Обработка ошибки (rejected) .addCase(fetchUsers.rejected, (state, action) => { state.isLoading = false; state.isError = true; // Сохраняем сообщение об ошибке. Используем action.payload, т.к. использовали rejectWithValue state.error = action.payload || 'Something went wrong'; }); }, }); // Экспортируем сгенерированный редьюсер (для стора) и синхронные экшены export const { clearError } = usersSlice.actions; export default usersSlice.reducer;
Наш слайс готов обрабатывать все стадии асинхронного запроса. Благодаря Immer (который встроен в RTK), мы можем обновлять состояние мутабельным образом, как в примере state.isLoading = true, но под капотом это превращается в корректное иммутабельное обновление.
Шаг 3: Используем thunk в React-компоненте
Осталось самое приятное, использовать наш асинхронный экшен в компоненте. Для этого мы воспользуемся уже знакомыми хуками useDispatch и useSelector.
// features/users/UserList.jsx import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { fetchUsers, clearError } from './usersSlice'; const UserList = () => { const dispatch = useDispatch(); // Выбираем нужные части состояния из стора const { items: users, isLoading, isError, error } = useSelector((state) => state.users); // Эффект для загрузки пользователей при монтировании компонента useEffect(() => { dispatch(fetchUsers()); }, [dispatch]); const handleRetry = () => { dispatch(clearError()); dispatch(fetchUsers()); }; // Рендерим состояние загрузки if (isLoading) { return <div className="loading">Загрузка пользователей...</div>; } // Рендерим состояние ошибки if (isError) { return ( <div className="error"> <p>Ошибка при загрузке: {error}</p> <button onClick={handleRetry}>Попробовать снова</button> </div> ); } // Рендерим список пользователей return ( <div> <h2>Список пользователей</h2> <ul> {users.map((user) => ( <li key={user.id}> <strong>{user.name}</strong> ({user.email}) </li> ))} </ul> </div> ); }; export default UserList;
Что происходит в компоненте:
-
Мы импортируем наш thunk
fetchUsersи синхронный экшенclearError. -
С помощью
useSelectorподписываемся на часть состоянияstate.users. -
В
useEffectпри первом рендере диспатчимfetchUsers(). Это запускает весь жизненный цикл thunk’а. -
В зависимости от состояния (
isLoading,isError), мы рендерим разный UI: индикатор загрузки, сообщение об ошибке с кнопкой для повтора или, наконец, сам список пользователей.
Это классический и очень надежный паттерн для работы с асинхронными данными в Redux.
Практические задания для закрепления
Чтобы материал точно отложился в голове, я подготовил для тебя несколько практических задач. Попробуй выполнить их самостоятельно.
Задание 1: Базовая интеграция
-
Создай новый проект или открой существующий, где настроены Redux Toolkit и React.
-
Реализуй слайс для загрузки пользователей в точности так, как показано в уроке.
-
Создай компонент
UserListи подключи его в свое приложение. Убедись, что список пользователей корректно загружается и отображается.
Задание 2: Обработка деталей пользователя
-
Расширь свой thunk
fetchUsers. Добавь второй thunk под названиемfetchUserById, который будет приниматьuserIdв качестве аргумента и загружать данные одного пользователя по эндпоинтуhttps://jsonplaceholder.typicode.com/users/${userId}. -
Создай в слайсе новое состояние для хранения данных об одном пользователе (например,
currentUser: null,isUserLoading: falseи т.д.). -
С помощью
extraReducersобработай все три состояния дляfetchUserById. -
Создай новый компонент
UserDetails, который по нажатию на пользователя в списке (или по ссылке) загружает и показывает его детальную информацию.
Задание 3: Улучшение UX
-
Добавь в состояние слайса флаг
hasFetched(булевый). Меняй его наtrueтолько при успешной загрузке (fulfilled). -
Измени логику в
useEffectкомпонентаUserList: еслиhasFetched === true, не делать повторный запрос при монтировании. Это поможет избежать лишних запросов при переходе между страницами, если данные уже загружены. -
В компоненте ошибки добавь кнопку «Повторить», как показано в примере, которая сбрасывает ошибку и заново запускает запрос.
Задание 4: Работа с формами и отправка данных
-
Создай новый thunk
addNewUserс помощьюcreateAsyncThunk. Он должен имитировать отправку POST-запроса для создания нового пользователя. Поскольку JSONPlaceholder не сохраняет данные, можно использоватьhttps://jsonplaceholder.typicode.com/usersкак POST-эндпоинт. Он вернет тебе фиктивный ответ, как будто пользователь создан. -
В
payload creatorпередавай объект с данными нового пользователя (name, email). -
Обработай
addNewUser.fulfilledвextraReducers. В этом обработчике «добавь» нового пользователя (изaction.payload) в массивstate.items. Подсказка: Так как это фейковый API, у нового пользователя не будет id от сервера. Можешь сгенерировать временный id на клиенте, прежде чем «отправлять» его. -
Создай компонент
AddUserFormс формой для ввода имени и почты. При сабмите формы диспатчьaddNewUserс данными из формы.
Итоги 9-го урока
Ты только что освоил один из самых основных инструментов в Redux Toolkit, это createAsyncThunk. Теперь ты умеешь:
-
Понимать проблему асинхронности в Redux.
-
Создавать асинхронные операции с помощью
createAsyncThunk. -
Обрабатывать три жизненных цикла запроса (pending, fulfilled, rejected) в
extraReducersслайса. -
Строить надежные интерфейсы, которые корректно реагируют на состояние загрузки и возможные ошибки.
Это навык для создания настоящих, живых приложений. В следующих уроках мы поговорим о структуре больших проектов и познакомимся с еще более продвинутых инструментом для работы с API, это RTK Query, который во многих случаях может полностью заменить createAsyncThunk. Но понимание thunk’ов это основа, которая поможет теглубоко понять и оценить преимущества RTK Query.
Не забывай выполнять практические задания, именно в практике рождается настоящее понимание. Если возникнут вопросы, не стесняйся пересматривать урок и искать дополнительную информацию.
Полный курс с уроками по Redux и Redux Toolkit для начинающих
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


