JavaRush /Курсы /Модуль 3: React /Типизация `createAsyncThunk` для работы с асинхронными да...

Типизация `createAsyncThunk` для работы с асинхронными данными

Модуль 3: React
6 уровень , 7 лекция
Открыта

Разбор типов данных в createAsyncThunk

Представьте, что мы пишем Redux-экшен для получения списка пользователей из API. Пользовательские данные могут выглядеть так:

interface User {
  id: number;
  name: string;
  email: string;
}

Для работы с асинхронным экшеном createAsyncThunk, нужно учитывать несколько основных моментов:

  1. Типизация данных, которые экшен "принимает" (параметры запроса).
  2. Типизация данных, которые экшен "возвращает" (ответ API).
  3. Типизация потенциальных ошибок.

Шаг 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 принимает:

  1. Уникальный идентификатор для экшена (строка).
  2. Функцию, возвращающую промис (обычно это запрос к 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'); // Возврат ошибки в состояние
    }
  }
);

В этом коде мы типизировали:

  1. Возвращаемые данныеFetchUsersResponse, в нашем случае массив объектов User.
  2. Входные данные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>
  );
};

Теперь вы можете отправить асинхронный экшен, отобразить данные или обработать ошибки в компоненте.

Типичные ошибки и как их избежать

  1. Неявные типы данных. Если не указать типы createAsyncThunk, TypeScript не сможет защитить вас от ошибок в данных (например, неправильной структуры API-ответа).
  2. Исключение обработки ошибок. Если не использовать rejectWithValue, то отлов ошибок и их типизация становится сложнее.
  3. Игнорирование зависимостей. Убедитесь, что все параметры явно описаны (например, filterByRole), даже если они необязательные.
1
Задача
Модуль 3: React, 6 уровень, 7 лекция
Недоступна
Обработка ошибок с rejectWithValue
Обработка ошибок с rejectWithValue
1
Задача
Модуль 3: React, 6 уровень, 7 лекция
Недоступна
Интеграция createAsyncThunk с состоянием
Интеграция createAsyncThunk с состоянием
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ