JavaRush /Курсы /Модуль 3: React /Типизация createAsyncThunk и обработка ошибок

Типизация createAsyncThunk и обработка ошибок

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

Структура createAsyncThunk

createAsyncThunk принимает два обязательных параметра:

  1. Имя действия (action type): строка, которая идентифицирует действие.
  2. Асинхронный 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:

  1. pending (загрузка началась),
  2. fulfilled (загрузка успешно завершена),
  3. 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;

Объяснение кода:

  1. Типизация состояния: интерфейс UserState четко указывает на структуру состояния, содержащую пользователя, состояние загрузки и ошибки.
  2. Использование builder.addCase:
    • для каждого из состояний pending, fulfilled, rejected мы добавили соответствующие обработчики.
    • поле action.error.message предоставляет информацию об ошибке, если она произошла.

Обработка ошибок в createAsyncThunk

Откуда берутся ошибки?

Ошибки могут возникать по нескольким причинам:

  1. Сервер возвращает ошибку (например, код 404 или 500).
  2. Ошибка в сети (нет подключения к интернету).
  3. Ошибка в логике клиента.

Использование 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), чтобы логировать серьезные ошибки, которые помогают анализировать поведение приложения в реальных условиях.

Основные типичные ошибки

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