Асинхронные процессы на клиенте
Асинхронные действия — это операции, выполнение которых занимает время. Например, запрос данных с сервера, отправка формы или обновление профиля пользователя. Они могут включать несколько состояний, таких как:
- Загрузка данных (loading),
- Успешное завершение (success),
- Ошибка (error).
Для таких операций раньше мы могли использовать middleware, такие как redux-thunk, и писать много шаблонного кода. Но зачем усложнять себе жизнь, когда есть createAsyncThunk, который упрощает этот процесс.
Преимущества createAsyncThunk
- Упрощает создание асинхронных операций.
- Автоматически обрабатывает состояния загрузки, успешного и ошибочного завершения.
- Поддерживает строгую типизацию с TypeScript.
- Интегрируется с
createSliceдля управления состоянием.
Создание асинхронного экшена с createAsyncThunk
Давайте начнём с практического примера. Мы добавим функциональность для загрузки данных пользователей из API.
Обязательно убедитесь, что у вас установлен @reduxjs/toolkit и настроен Store.
Создаём асинхронный экшен
Добавим новый срез usersSlice для работы с данными пользователей. Начнём с createAsyncThunk.
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
interface User {
id: number;
name: string;
email: string;
}
interface UsersState {
users: User[];
loading: boolean;
error: string | null;
}
const initialState: UsersState = {
users: [],
loading: false,
error: null,
};
// Асинхронный экшен для получения пользователей
export const fetchUsers = createAsyncThunk<User[], void>(
'users/fetchUsers',
async (_, { rejectWithValue }) => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) {
throw new Error('Ошибка загрузки данных');
}
return response.json();
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);
Разбор кода:
- Импорт
createAsyncThunk: это основной инструмент для создания асинхронных экшенов. - Типизация: мы указываем, что
fetchUsersвозвращает массив пользователейUser[]. - rejectWithValue: позволяет передать ошибку в случай обработки неудачи (об этом чуть позже).
Добавление обработчиков в Slice
Теперь подключим наш асинхронный экшен к usersSlice. Мы опишем, как состояние должно изменяться на каждом этапе выполнения.
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {}, // Здесь можно добавить обычные редьюсеры
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchUsers.fulfilled, (state, action: PayloadAction<User[]>) => {
state.loading = false;
state.users = action.payload;
})
.addCase(fetchUsers.rejected, (state, action: PayloadAction<any>) => {
state.loading = false;
state.error = action.payload;
});
},
});
export const usersReducer = usersSlice.reducer;
Что происходит:
fetchUsers.pending: срабатывает, когда запрос отправлен, но данные ещё не загружены. Мы выставляемloadingвtrueи очищаем ошибку.fetchUsers.fulfilled: вызвается, если данные успешно загружены. Мы сохраняем пользователей в состоянии.fetchUsers.rejected: если запрос завершился ошибкой, мы сохраняем сообщение об ошибке и отключаем индикатор загрузки.
Интеграция с компонентами
Шаг 1: Подключаем редьюсер в Store
В store.ts добавьте новый редьюсер:
import { configureStore } from '@reduxjs/toolkit';
import { usersReducer } from './usersSlice';
export const store = configureStore({
reducer: {
users: usersReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Шаг 2: Используем FetchUsers в компоненте
Теперь создадим компонент UsersList, который будет отображать пользователей.
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchUsers } from './usersSlice';
import { RootState, AppDispatch } from './store';
const UsersList: React.FC = () => {
const dispatch = useDispatch<AppDispatch>();
const { users, loading, error } = useSelector((state: RootState) => state.users);
useEffect(() => {
dispatch(fetchUsers());
}, [dispatch]);
if (loading) {
return <p>Загрузка...</p>;
}
if (error) {
return <p>Ошибка: {error}</p>;
}
return (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
);
};
export default UsersList;
Разбор:
useEffectсdispatch(fetchUsers()): мы вызываем асинхронный экшен в момент монтирования компонента.- Доступ к данным:
useSelectorизвлекает пользователей, состояние загрузки и ошибки из Store. - Рендеринг состояния: мы показываем индикатор загрузки, сообщение об ошибке или список пользователей в зависимости от состояния.
Реальные кейсы и советы
- Обработка ошибок. Всегда обрабатывайте ошибки с помощью
rejectWithValue, чтобы иметь ясное сообщение об ошибке в состоянии. - Повторное использование. Асинхронные экшены могут быть переиспользованы в разных частях приложения.
- Типизация. Правильная типизация упрощает использование данных и предотвращает ошибки.
- Оптимизация. Используйте Selectors для мемоизации данных и сокращения перерисовок компонентов.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ