Структура createAsyncThunk
createAsyncThunk принимает два обязательных параметра:
- Имя действия (action type): строка, которая идентифицирует действие.
- Асинхронный payload creator: функция, которая выполняет асинхронную логику.
Пример:
import { createAsyncThunk } from '@reduxjs/toolkit';
interface User {
id: number;
name: string;
}
export const fetchUser = createAsyncThunk<User, number>(
'user/fetchById',
async (userId) => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
return (await response.json()) as User;
}
);
Разберем этот пример по частям:
User— тип результата, который вернетcreateAsyncThunk.number— тип аргумента, который принимает наша асинхронная функцияuserId.- внутри
async-функции мы делаем запрос к API и возвращаем JSON, соответствующий типуUser.
Типизация результатов и состояний загрузки
Теперь посмотрим, как типизировать три стандартных состояния createAsyncThunk:
- pending (загрузка началась),
- fulfilled (загрузка успешно завершена),
- rejected (произошла ошибка).
Для хранения состояния мы обычно добавляем нужные поля в срез:
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { fetchUser } from './thunks';
interface UserState {
user: User | null;
loading: boolean;
error: string | null;
}
const initialState: UserState = {
user: null,
loading: false,
error: null,
};
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchUser.fulfilled, (state, action: PayloadAction<User>) => {
state.loading = false;
state.user = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || 'Unknown error';
});
},
});
export default userSlice.reducer;
Объяснение кода:
- Типизация состояния: интерфейс
UserStateчетко указывает на структуру состояния, содержащую пользователя, состояние загрузки и ошибки. - Использование
builder.addCase:- для каждого из состояний
pending,fulfilled,rejectedмы добавили соответствующие обработчики. - поле
action.error.messageпредоставляет информацию об ошибке, если она произошла.
- для каждого из состояний
Обработка ошибок в createAsyncThunk
Откуда берутся ошибки?
Ошибки могут возникать по нескольким причинам:
- Сервер возвращает ошибку (например, код 404 или 500).
- Ошибка в сети (нет подключения к интернету).
- Ошибка в логике клиента.
Использование try-catch
Вам может понадобиться обрабатывать ошибки вручную внутри createAsyncThunk. Здесь приходит на помощь rejectWithValue. Это полезно, когда вы хотите передать клиенту ошибки, полученные от сервера.
Пример:
import { createAsyncThunk } from '@reduxjs/toolkit';
interface ApiError {
message: string;
statusCode: number;
}
export const fetchUserWithErrorHandling = createAsyncThunk<User, number, { rejectValue: ApiError }>(
'user/fetchWithErrorHandling',
async (userId, { rejectWithValue }) => {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
// Создаем объект ошибки
const error: ApiError = {
message: 'Failed to fetch user',
statusCode: response.status,
};
return rejectWithValue(error);
}
return (await response.json()) as User;
} catch (err) {
// Обрабатываем любые другие ошибки
return rejectWithValue({
message: 'Something went wrong',
statusCode: 500,
});
}
}
);
Обработка rejectWithValue в extraReducers
Когда вы передаете значение через rejectWithValue, оно становится доступным в action.payload в редюсере состояния.
builder.addCase(fetchUserWithErrorHandling.rejected, (state, action) => {
state.loading = false;
state.error = action.payload ? action.payload.message : 'Unknown error';
});
Такой подход позволяет удобнее работать с детализированными ошибками, например, использовать разные подсказки для пользователей в зависимости от кода ошибки.
Полезные приемы и советы
Обработка состояния загрузки
Не забывайте про индикаторы загрузки! Например:
import React from 'react';
import { useSelector } from 'react-redux';
import { RootState } from './store';
const UserComponent: React.FC = () => {
const { user, loading, error } = useSelector((state: RootState) => state.user);
return (
<div>
{loading && <p>Loading...</p>}
{error && <p style={{ color: 'red' }}>{error}</p>}
{user && <p>Welcome, {user.name}!</p>}
</div>
);
};
Логирование ошибок
Используйте внешние библиотеки (например, Sentry или Bugsnag), чтобы логировать серьезные ошибки, которые помогают анализировать поведение приложения в реальных условиях.
Основные типичные ошибки
- Отсутствие типизации: если вы не укажете типы данных, вы столкнетесь с неявными ошибками в вашем коде. Используйте интерфейсы и дженерики!
- Игнорирование ошибок: никогда не оставляйте ошибки необработанными. Разработчикам важно знать, что пошло не так.
- Лишняя сложность: не переусложняйте обработку ошибок. Используйте простые подходы, такие как
rejectWithValue.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ