JavaRush /Курсы /Модуль 3: React /Создание эффектов (call, put, takeEvery) в Redux Saga

Создание эффектов (call, put, takeEvery) в Redux Saga

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

Подробнее про эффекты в Redux Saga

Эффекты в Redux Saga — это инструменты, которые помогают управлять асинхронными и синхронными процессами. Они предоставляют декларативный способ описания побочных эффектов (например, вызова API, отправки действий или ожидания других действий) в вашем приложении.

В отличие от Thunk, где вы просто пишете функции с асинхронным кодом, эффекты Saga используют подход на основе генераторов (специальный тип функций в JavaScript), чтобы более понятно и структурировано выполнять сложные процессы.

Основные эффекты:

Эффект Назначение
call Вызов функции (например, API-запроса) с передачей аргументов.
put Отправка действия (аналог dispatch) для изменения состояния через редьюсер.
takeEvery Прослушивание всех экземпляров определённого действия и вызов соответствующей саги для каждого.

Теперь давайте разберём каждый из них с практическими примерами. И, конечно же, сделаем это с TypeScript — без типизации мы никуда.

1. Эффект call: вызов функций

call используется, чтобы вызывать функции внутри саги. Это может быть полезно для выполнения API-запросов или вызова других функций, требующих выполнения.

Допустим, у нас есть API-функция fetchData:

// api.ts
export const fetchData = async (id: string): Promise<{ id: string; name: string }> => {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
  if (!response.ok) throw new Error('Failed to fetch data');
  return response.json();
};

Теперь напишем сагу, которая будет вызывать эту функцию с использованием call:

import { call, put } from 'redux-saga/effects';
import { fetchData } from './api';
import { fetchSuccess, fetchFailure } from './actions';

function* fetchDataSaga(action: { type: string; payload: string }) {
  try {
    // Используем call для вызова fetchData и передаём id как аргумент
    const data = yield call(fetchData, action.payload);
    // Отправляем действие с результатом
    yield put(fetchSuccess(data));
  } catch (error) {
    // Обрабатываем ошибку и отправляем другое действие
    yield put(fetchFailure((error as Error).message));
  }
}

Здесь call принимает два аргумента: саму функцию fetchData и её параметры action.payload. Это делает код более декларативным и удобным для тестирования.

2. Эффект put: отправка действий

put используется для отправки действий, аналогично тому, как это делает dispatch. С его помощью мы можем уведомлять Redux о результатах выполнения асинхронных операций.

Вернёмся к нашей саге fetchDataSaga:

function* fetchDataSaga(action: { type: string; payload: string }) {
  try {
    const data = yield call(fetchData, action.payload);
    // Отправляем действие с результатом через put
    yield put(fetchSuccess(data));
  } catch (error) {
    // Отправляем действие с ошибкой
    yield put(fetchFailure((error as Error).message));
  }
}

Здесь мы используем put дважды: первый раз для отправки действия fetchSuccess, если данные успешно загружены, а второй раз для отправки действия fetchFailure, если произошла ошибка.

Если бы мы делали это без Redux Saga, нам нужно было бы вручную вызывать dispatch в компоненте. С put всё происходит внутри саги.

3. Эффект takeEvery: обработка каждого действия

Эффект takeEvery позволяет вам "подписаться" на определённые действия и выполнять сагу каждый раз, когда это действие отправляется. Это похоже на вечеринку, где "каждое действие" — это приглашённый гость, а takeEvery отвечает за то, чтобы их всех встретить и проводить к саге.

Создадим прослушиватель для действия FETCH_DATA_REQUEST:

import { takeEvery } from 'redux-saga/effects';

function* watchFetchDataSaga() {
  // takeEvery будет вызывать fetchDataSaga для каждого действия FETCH_DATA_REQUEST
  yield takeEvery('FETCH_DATA_REQUEST', fetchDataSaga);
}

Интеграция с Redux Saga

Теперь нужно объединить все саги с помощью rootSaga. Это похоже на главный пульт управления, куда стекаются все потоки:

import { all } from 'redux-saga/effects';
import { watchFetchDataSaga } from './fetchDataSaga';

export function* rootSaga() {
  yield all([
    watchFetchDataSaga(), // Добавляем подписанную сагу
  ]);
}

Полный пример: работа с API и управление состоянием

Давайте соберём всё вместе: создадим Redux-приложение, которое будет загружать данные пользователя по ID с помощью Redux Saga.

1. Действия

// actions.ts
export const fetchDataRequest = (id: string) => ({ type: 'FETCH_DATA_REQUEST', payload: id });
export const fetchSuccess = (data: { id: string; name: string }) => ({ type: 'FETCH_SUCCESS', payload: data });
export const fetchFailure = (error: string) => ({ type: 'FETCH_FAILURE', payload: error });

2. Редьюсер

// reducer.ts
interface State {
  data: { id: string; name: string } | null;
  loading: boolean;
  error: string | null;
}

const initialState: State = {
  data: null,
  loading: false,
  error: null,
};

export const fetchReducer = (state = initialState, action: any): State => {
  switch (action.type) {
    case 'FETCH_DATA_REQUEST':
      return { ...state, loading: true, error: null };
    case 'FETCH_SUCCESS':
      return { ...state, loading: false, data: action.payload };
    case 'FETCH_FAILURE':
      return { ...state, loading: false, error: action.payload };
    default:
      return state;
  }
};

3. Сага

import { call, put, takeEvery } from 'redux-saga/effects';
import { fetchData } from './api';
import { fetchDataRequest, fetchSuccess, fetchFailure } from './actions';

function* fetchDataSaga(action: ReturnType<typeof fetchDataRequest>) {
  try {
    const data = yield call(fetchData, action.payload);
    yield put(fetchSuccess(data));
  } catch (error) {
    yield put(fetchFailure((error as Error).message));
  }
}

export function* watchFetchDataSaga() {
  yield takeEvery('FETCH_DATA_REQUEST', fetchDataSaga);
}

4. Подключение к Redux Store

import { configureStore } from '@reduxjs/toolkit';
import createSagaMiddleware from 'redux-saga';
import { fetchReducer } from './reducer';
import { rootSaga } from './sagas';

const sagaMiddleware = createSagaMiddleware();

export const store = configureStore({
  reducer: {
    fetch: fetchReducer,
  },
  middleware: [sagaMiddleware],
});

sagaMiddleware.run(rootSaga);

5. Компонент

import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchDataRequest } from './actions';
import { RootState } from './store';

const UserComponent: React.FC = () => {
  const [userId, setUserId] = useState('');
  const dispatch = useDispatch();
  const { data, loading, error } = useSelector((state: RootState) => state.fetch);

  const handleFetch = () = >{
    dispatch(fetchDataRequest(userId));
  };

  return (
    <div>
      <input
        type="text"
        placeholder="Enter user ID"
        value={userId}
        onChange={(e) => setUserId(e.target.value)}
      />
      <button onClick={handleFetch} disabled={loading}>
        {loading ? 'Loading...' : 'Fetch User'}
      </button>
      {data && <div>User: {data.name}</div>}
      {error && <div>Error: {error}</div>}
    </div>
  );
};

export default UserComponent;

В этом примере мы смогли загрузить данные и встроили хороший поток типизации для всех созданных функций. Кстати, вы заметили, как мы легко подхватили поток асинхронных действий с помощью call, put и takeEvery? Эти эффекты становятся основой эффективной структуры управления в Redux Saga.

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