← Назад к вопросам

Что такое функции-генераторы в Redux Saga?

2.2 Middle🔥 92 комментариев
#JavaScript Core#State Management

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое функции-генераторы в Redux Saga?

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

Основы функций-генераторов

// Синтаксис функции-генератора
function* myGenerator() {
  const value1 = yield 'первое значение';
  const value2 = yield 'второе значение';
  return 'конец';
}

// Генератор НЕ выполняется сразу
const gen = myGenerator();
console.log(gen); // Object [Generator]

// Вызов next() выполняет код до первого yield
const step1 = gen.next();
console.log(step1); // { value: 'первое значение', done: false }

const step2 = gen.next();
console.log(step2); // { value: 'второе значение', done: false }

const step3 = gen.next();
console.log(step3); // { value: 'конец', done: true }

Ключевое свойство генераторов: они паузируются на yield и возобновляются при следующем вызове next().

Как Redux Saga использует генераторы

// Классический Redux Thunk (без Saga)
function fetchUserThunk(userId) {
  return async (dispatch) => {
    dispatch({ type: 'LOADING' });
    try {
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      dispatch({ type: 'SUCCESS', payload: data });
    } catch (error) {
      dispatch({ type: 'ERROR', payload: error });
    }
  };
}

// Redux Saga (с генератором)
function* fetchUserSaga(action) {
  try {
    yield put({ type: 'LOADING' });
    
    const response = yield fetch(`/api/users/${action.payload}`);
    const data = yield response.json();
    
    yield put({ type: 'SUCCESS', payload: data });
  } catch (error) {
    yield put({ type: 'ERROR', payload: error });
  }
}

Преимущества Saga:

  1. Синхронный стиль кода (легче читать)
  2. Легче тестировать (мокируем значения в yield)
  3. Лучше управление побочными эффектами
  4. Встроенная отмена операций

Redux Saga эффекты (Effects)

Redux Saga предоставляет специальные функции для работы с побочными эффектами:

import { put, call, select, take, fork } from 'redux-saga/effects';

// 1. put() — отправка действия (dispatch)
function* mySaga() {
  yield put({ type: 'INCREMENT' });  // Эквивалент dispatch()
}

// 2. call() — вызов функции
function fetchUser(userId) {
  return fetch(`/api/users/${userId}`).then(r => r.json());
}

function* mySaga() {
  const user = yield call(fetchUser, 123);
  console.log(user);
}

// 3. select() — доступ к state
function* mySaga() {
  const state = yield select();
  const currentUser = yield select(state => state.user);
}

// 4. take() — ожидание действия
function* mySaga() {
  const action = yield take('INCREMENT');  // Ждет действия
  console.log('INCREMENT произошел:', action);
}

// 5. fork() — запуск параллельного процесса
function* mySaga() {
  // Не ждет завершения, просто запускает
  yield fork(backgroundTask);
  
  // Код продолжает выполняться
}

Полный пример Redux Saga

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

// 1. API функция
const fetchUserAPI = (userId) => 
  fetch(`/api/users/${userId}`).then(r => r.json());

// 2. Saga генератор
function* fetchUserSaga(action) {
  try {
    // call() выполняет функцию и ждет результат
    const user = yield call(fetchUserAPI, action.payload);
    
    // put() отправляет действие в Redux
    yield put({ 
      type: 'FETCH_USER_SUCCESS', 
      payload: user 
    });
  } catch (error) {
    yield put({ 
      type: 'FETCH_USER_ERROR', 
      payload: error.message 
    });
  }
}

// 3. Вотчер saga
function* userSagaWatcher() {
  // takeEvery слушает действия FETCH_USER_REQUEST
  yield takeEvery('FETCH_USER_REQUEST', fetchUserSaga);
}

// 4. Регистрация в store
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));

// Запуск saga
sagaMiddleware.run(userSagaWatcher);

Контроль потока в Saga

// 1. takeEvery — обрабатывает каждое действие
function* watchAndFetch() {
  yield takeEvery('FETCH_DATA', fetchData);
  // Если пользователь кликнет 10 раз, сработает 10 запросов
}

// 2. takeLatest — отменяет предыдущий, если новый пришел
function* watchAndFetch() {
  yield takeLatest('FETCH_DATA', fetchData);
  // Если пользователь кликнет 10 раз быстро, сработает только последний
}

// 3. takeLeading — игнорирует новые, пока не завершится текущий
function* watchAndFetch() {
  yield takeLeading('FETCH_DATA', fetchData);
  // Первый клик работает, остальные игнорируются до завершения
}

Тестирование Saga с генераторами

// Функция Saga
function* fetchUserSaga(action) {
  try {
    const user = yield call(fetchUserAPI, action.payload);
    yield put({ type: 'SUCCESS', payload: user });
  } catch (error) {
    yield put({ type: 'ERROR', payload: error });
  }
}

// Тест
import { call, put } from 'redux-saga/effects';

describe('fetchUserSaga', () => {
  it('должен fetch пользователя и dispatch SUCCESS', () => {
    const action = { payload: 1 };
    const gen = fetchUserSaga(action);
    
    // Шаг 1: проверяем, что вызывается API
    let result = gen.next();
    expect(result.value).toEqual(call(fetchUserAPI, 1));
    
    // Шаг 2: мокируем результат API
    const user = { id: 1, name: 'John' };
    result = gen.next(user);
    expect(result.value).toEqual(
      put({ type: 'SUCCESS', payload: user })
    );
    
    // Шаг 3: проверяем завершение
    result = gen.next();
    expect(result.done).toBe(true);
  });
  
  it('должен dispatch ERROR при ошибке', () => {
    const action = { payload: 1 };
    const gen = fetchUserSaga(action);
    
    gen.next(); // Пропускаем call()
    
    // Мокируем ошибку
    const error = new Error('Network error');
    let result = gen.throw(error);
    
    expect(result.value).toEqual(
      put({ type: 'ERROR', payload: error })
    );
  });
});

Сложные сценарии с Saga

1. Отмена операции (Race)

import { race, delay } from 'redux-saga/effects';

function* fetchWithTimeout(userId) {
  const { response, timeout } = yield race({
    response: call(fetchUserAPI, userId),
    timeout: delay(5000)  // 5 секунд
  });
  
  if (timeout) {
    yield put({ type: 'FETCH_TIMEOUT' });
  } else {
    yield put({ type: 'FETCH_SUCCESS', payload: response });
  }
}

2. Параллельные операции (All)

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

function* fetchBothUserAndPosts(userId) {
  const [user, posts] = yield all([
    call(fetchUserAPI, userId),
    call(fetchPostsAPI, userId)
  ]);
  
  yield put({
    type: 'FETCH_COMPLETE',
    payload: { user, posts }
  });
}

3. Поллинг (повторные попытки)

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

function* pollData(userId) {
  while (true) {
    try {
      const data = yield call(fetchUserAPI, userId);
      yield put({ type: 'POLL_SUCCESS', payload: data });
    } catch (error) {
      console.log('Poll error, retry in 5s');
    }
    
    // Ждем 5 секунд
    yield delay(5000);
  }
}

Генераторы против async/await

// Генератор + Saga
function* fetchUserSaga(userId) {
  try {
    const user = yield call(fetchUserAPI, userId);
    yield put({ type: 'SUCCESS', payload: user });
  } catch (error) {
    yield put({ type: 'ERROR', payload: error });
  }
}

// async/await эквивалент (более простой)
async function fetchUserAsync(userId) {
  try {
    const user = await fetchUserAPI(userId);
    dispatch({ type: 'SUCCESS', payload: user });
  } catch (error) {
    dispatch({ type: 'ERROR', payload: error });
  }
}

Когда выбирать Saga:

  • Сложные асинхронные потоки
  • Нужна отмена операций
  • Множество параллельных задач
  • Интеграция со множеством API

Когда выбирать async/await + Redux Thunk:

  • Простая логика
  • Новый проект
  • Меньше boilerplate

Заключение

Функции-генераторы в Redux Saga:

  1. Синтаксис: function* с использованием yield
  2. Контроль: Паузируются и возобновляются по demand
  3. Эффекты: put, call, select, take, fork для управления побочными эффектами
  4. Тестируемость: Легко тестировать, проверяя последовательность эффектов
  5. Контроль потока: takeEvery, takeLatest, race, all для сложных сценариев
  6. Отмена: Встроенная поддержка отмены асинхронных операций

Генераторы в Saga позволяют писать асинхронный код, выглядящий синхронно, что улучшает читаемость и тестируемость сложных приложений.