Разбор типов данных в createAsyncThunk
Представьте, что мы пишем Redux-экшен для получения списка пользователей из API. Пользовательские данные могут выглядеть так:
interface User {
id: number;
name: string;
email: string;
}
Для работы с асинхронным экшеном createAsyncThunk, нужно учитывать несколько основных моментов:
- Типизация данных, которые экшен "принимает" (параметры запроса).
- Типизация данных, которые экшен "возвращает" (ответ API).
- Типизация потенциальных ошибок.
Шаг 1: Создание интерфейсов для типизации
Давайте начнём с объявлений типов. В Redux Toolkit используется стандартное соглашение, где состояния называют:
idle— состояние без запросов;loading— запрос выполняется;succeeded— запрос успешно завершён;failed— запрос завершился ошибкой.
Добавим это в наш тип состояния.
interface UsersState {
users: User[]; // Список пользователей
status: 'idle' | 'loading' | 'succeeded' | 'failed'; // Состояние запроса
error: string | null; // Сообщение об ошибке
}
Теперь у нас есть описание состояния. Для действий createAsyncThunk, помимо состояния, нужно также типизировать данные запроса и ответа. Например, если мы передаём ID пользователя для фильтрации, это можно описать как:
interface FetchUsersParams {
filterByRole?: string; // Фильтрация по роли, необязательный параметр
}
И предполагаемый ответ от API:
type FetchUsersResponse = User[]; // Массив объектов User
Шаг 2: Использование createAsyncThunk с типами
Теперь напишем асинхронное действие. В Redux Toolkit createAsyncThunk принимает:
- Уникальный идентификатор для экшена (строка).
- Функцию, возвращающую промис (обычно это запрос к API).
Типизация createAsyncThunk выглядит следующим образом:
createAsyncThunk<ReturnedType, InputType, ThunkApiConfig>
ReturnedType— тип возвращаемых данных (успешный ответ).InputType— тип данных, передаваемых в экшен (параметры запроса).ThunkApiConfig— необязательный объект с настройками, такими какrejectValue(типизация ошибок).
Пример:
import { createAsyncThunk } from '@reduxjs/toolkit';
// Экшен для получения пользователей
export const fetchUsers = createAsyncThunk<FetchUsersResponse, FetchUsersParams>(
'users/fetchUsers', // Уникальный идентификатор
async (params, thunkAPI) => {
try {
const response = await fetch(`https://api.example.com/users?role=${params.filterByRole ?? ''}`);
if (!response.ok) {
return thunkAPI.rejectWithValue('Failed to fetch users'); // Обработка ошибки
}
const data: FetchUsersResponse = await response.json();
return data;
} catch (error) {
return thunkAPI.rejectWithValue('Network error'); // Возврат ошибки в состояние
}
}
);
В этом коде мы типизировали:
- Возвращаемые данные —
FetchUsersResponse, в нашем случае массив объектовUser. - Входные данные —
FetchUsersParams, объект с параметрами для фильтрации.
Шаг 3: Добавление ошибок с rejectWithValue
Иногда нам нужно передать конкретное значение ошибки в состояние. Это можно сделать с помощью rejectWithValue. Например, если ошибка — строка:
export const fetchUsers = createAsyncThunk<
FetchUsersResponse,
FetchUsersParams,
{ rejectValue: string }
>(
'users/fetchUsers',
async (params, thunkAPI) => {
try {
const response = await fetch(`https://api.example.com/users?role=${params.filterByRole ?? ''}`);
if (!response.ok) {
return thunkAPI.rejectWithValue('Failed to fetch users'); // Укажем типизируемую ошибку
}
const data: FetchUsersResponse = await response.json();
return data;
} catch (error) {
return thunkAPI.rejectWithValue('Network error');
}
}
);
Теперь каждый вызов thunkAPI.rejectWithValue ожидает строку в качестве значения ошибки.
Шаг 4: Обновление Slice с состоянием
Чтобы отреагировать на результаты асинхронного экшена, нужно обновить редьюсер внутри createSlice. Redux Toolkit упрощает эту задачу с помощью дополнительных редьюсеров.
Мы добавим обработку состояний pending, fulfilled и rejected, которые автоматически создаются для каждого асинхронного экшена.
import { createSlice } from '@reduxjs/toolkit';
import { fetchUsers } from './thunks';
const initialState: UsersState = {
users: [],
status: 'idle',
error: null,
};
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.status = 'loading';
state.error = null; // Очистка прошлых ошибок
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'succeeded';
state.users = action.payload; // Запись данных в состояние
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload as string; // Сохранение ошибки
});
},
});
export default usersSlice.reducer;
Обратите внимание, что action.payload внутри rejected имеет тип, который мы указали в rejectWithValue.
Шаг 5: Подключение к компонентам
Теперь мы готовы использовать этот экшен в компонентах. Вот пример:
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchUsers } from './thunks';
import { RootState } from './store';
export const UsersList: React.FC = () => {
const dispatch = useDispatch();
const { users, status, error } = useSelector((state: RootState) => state.users);
useEffect(() => {
dispatch(fetchUsers({ filterByRole: 'admin' })); // Пример параметра
}, [dispatch]);
if (status === 'loading') return <p>Loading...</p>;
if (status === 'failed') return <p>Error: {error}</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li> // Вывод списка пользователей
))}
</ul>
);
};
Теперь вы можете отправить асинхронный экшен, отобразить данные или обработать ошибки в компоненте.
Типичные ошибки и как их избежать
- Неявные типы данных. Если не указать типы
createAsyncThunk, TypeScript не сможет защитить вас от ошибок в данных (например, неправильной структуры API-ответа). - Исключение обработки ошибок. Если не использовать
rejectWithValue, то отлов ошибок и их типизация становится сложнее. - Игнорирование зависимостей. Убедитесь, что все параметры явно описаны (например,
filterByRole), даже если они необязательные.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ