Thunk VS Saga: краткое сравнение с точки зрения реальной жизни
Подводя итоги, давайте погрузимся в сравнение и разбор практических кейсов применения Thunk и Saga. Постараемся понять, когда, зачем и почему использовать ту или иную библиотеку.
Представьте, что у вас есть два курьера, которым вы поручаете приносить вам пиццу из разных мест.
Thunk — это курьер с велосипедом: быстрый, простой и доступный. Он берет задание, едет в пиццерию, забирает пиццу и возвращает её вам. Но... если вы попросите его доставить пиццу и ещё параллельно купить молоко и газировку, ему будет сложновато. Thunk подходит для простых или средних задач, где не требуется слишком много логики.
Saga — это профессиональный шофер с машиной. Он умеет не только забирать пиццу, но и планировать маршрут, следить за временем, а если по дороге что-то пойдет не так (например, магазин закроется), он попробует альтернативные варианты. Saga подходит для сложных задач и более изощренных цепочек действий.
Теперь, зная, кто ваш идеальный курьер, давайте углубимся в реализацию и разберём, как выбрать подходящий инструмент.
Кейс #1: простое приложение с минимальной логикой
Ваше приложение — это, скажем, список задач (To-Do). Пользователи могут добавлять, удалять и изменять задачи. Всё это хранится в удалённой базе через API.
Подход: используем Redux Thunk!
Почему Thunk? Потому что реализация проста: вам нужно выполнить простой HTTP-запрос и обновить состояние. Вот пример:
// actions/todoActions.ts
import { Dispatch } from 'redux';
import { Todo } from '../types';
export const fetchTodos = () => {
return async (dispatch: Dispatch) => {
dispatch({ type: 'FETCH_TODOS_REQUEST' });
try {
const response = await fetch('/api/todos');
const data: Todo[] = await response.json();
dispatch({ type: 'FETCH_TODOS_SUCCESS', payload: data });
} catch (error) {
dispatch({ type: 'FETCH_TODOS_FAILURE', error: String(error) });
}
};
};
Типизация? Легко. Просто используем Dispatch из Redux. Для такого простого сценария Thunk прекрасно подходит: компонент отправляет запрос, запрос возвращает данные, всё обновляется. Быстро, эффективно и удобно.
Проблемы
Если логика запросов усложнится (например, появится необходимость обработать несколько API-запросов, чей результат влияет друг на друга), Thunk может стать запутанным и сложным для отладки.
Кейс #2: реакция на множество действий
Представьте приложение, которое получает данные о погоде. Вы должны:
- Запрашивать данные каждые 10 минут.
- Обработать ошибку, если API недоступно.
- Уведомлять пользователя о новой погоде.
Подход: это работа для Redux Saga
Почему Saga? Потому что она позволяет легко управлять сложными асинхронными процессами через генераторы. Вот как это можно сделать:
// sagas/weatherSaga.ts
import { call, put, takeEvery, delay } from 'redux-saga/effects';
import { fetchWeatherApi } from '../api/weatherApi';
import { WEATHER_REQUEST, WEATHER_SUCCESS, WEATHER_FAILURE } from '../actions/types';
function* fetchWeather(): Generator {
while (true) {
try {
const data = yield call(fetchWeatherApi);
yield put({ type: WEATHER_SUCCESS, payload: data });
} catch (error) {
yield put({ type: WEATHER_FAILURE, error });
}
yield delay(600000); // Повторить через 10 минут
}
}
export function* watchWeatherSaga(): Generator {
yield takeEvery(WEATHER_REQUEST, fetchWeather);
}
Почему это круто
Saga обрабатывает цикл без каких-либо лишних вмешательств. Вся логика гибко разделена: чтение данных с API, диспетчеризация действий, ошибки и повторные запросы. Попробуйте написать это же на Thunk? Получится большая плоскость вложенных функций и условий.
Кейс #3: взаимодействие между действиями
Предположим, вы создаёте приложение для e-commerce. После успешного логина пользователя вы хотите сразу загрузить его корзину и историю заказов. Но если логин не удался, запросы делать не нужно.
Подход: Redux Saga идеально подходит для этого.
Saga обеспечивает прямое управление потоком выполнения. Сначала проверяем успешность логина, если всё ок, начинаем загрузку корзины и истории. Вот пример:
// sagas/authSaga.ts
import { call, put, takeEvery, all } from 'redux-saga/effects';
import { loginApi, fetchCartApi, fetchOrdersApi } from '../api';
import { LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_FAILURE, FETCH_CART, FETCH_ORDERS } from '../actions/types';
function* login(action: any): Generator {
try {
const user = yield call(loginApi, action.payload);
yield put({ type: LOGIN_SUCCESS, payload: user });
// После успешного логина параллельно загружаем корзину и заказы
yield all([
put({ type: FETCH_CART }),
put({ type: FETCH_ORDERS })
]);
} catch (error) {
yield put({ type: LOGIN_FAILURE, error });
}
}
export function* watchAuthSaga(): Generator {
yield takeEvery(LOGIN_REQUEST, login);
}
С помощью call и all можно управлять несколькими процессами, используя минималистичный и понятный код.
Когда использовать Thunk или Saga?
Redux Thunk хорош для:
- Простых запросов (CRUD-операции).
- Маленьких приложений.
- Если вы хотите начать быстро и не ломать голову над новым инструментом.
Redux Saga подходит для:
- Сложных потоков данных.
- Последовательных или параллельных операций.
- Сложной обработки ошибок или повторных попыток.
- Массивных приложений, где вам потребуется легко расширять и поддерживать асинхронные процессы.
Типичные ошибки и их решения
Вложенные функции с Thunk. Если вы замечаете, что ваши Thunk-функции начинают напоминать спагетти, это верный признак того, что пора переключиться на Saga.
Слишком сложные саги. Иногда разработчики пытаются написать мега-сагу, которая управляет всем сразу. Это плохо. Разделяйте логику: одна сага — одна задача. Избыточная сложность приводит к трудностям в отладке.
Отсутствие типизации. В любом подходе, будь то Thunk или Saga, обязательно типизируйте состояния и действия. Это не только поможет избежать ошибок, но и существенно упростит понимание кода.
Итак, выбор между Thunk и Saga — это как выбор между велосипедом и машиной. Велосипед легче, дешевле и быстрее для коротких поездок, но если вам предстоит долгий и сложный путь (с препятствиями), лучше пересесть в машину. Выбор за вами и зависит от контекста вашего проекта и задач.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ