Подробнее про эффекты в 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.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ