Gernar
Frontend DeveloperRedux, состояние и данные

Интервью-вопрос

Как Redux Thunk и Redux Saga подключаются к Redux

Оба подхода подключаются как middleware к Redux store. Важно не только назвать пакет, но и объяснить, что меняется в `dispatch`, как запускается Saga и когда такой выбор оправдан.

Добавлен
Редакция

Подготовьте короткий ответ и пару деталей на случай уточняющих вопросов.

🐰0
🥚0

Мини-квиз

Проверка перед разбором

Несколько быстрых вопросов перед разбором. Так проще поймать места, которые только кажутся понятными.

Вопрос 1 из 50 правильно

Как лучше ответить про подключение Redux Thunk?

Вы объясняете настройку store в современном проекте на Redux Toolkit.

Варианты ответа

Разбор

Разобраться, а не зазубрить

Дальше разбираем суть, типичные уточнения и места, где легко сказать лишнее или перепутать термины.

Базовая идея

Redux сам по себе ожидает, что в dispatch попадет action object. Middleware стоит между dispatch и reducer. Он может перехватить action, выполнить побочную работу, изменить поток или передать action дальше.

Поэтому Thunk и Saga подключаются не как reducer и не как React-компонент. Они расширяют store через middleware. Разница в модели работы. Thunk исполняет функции, которые вы dispatch-ите. Saga запускает отдельные генераторы, которые слушают actions и выполняют эффекты.

Коротко можно ответить так: подключение идет через middleware, но запуск отличается. У Thunk достаточно middleware в store. У Saga есть два шага: добавить middleware и запустить root saga.

Как подключить Thunk

В Redux Toolkit thunk уже включен в default middleware. Поэтому для обычного thunk action creator чаще всего не нужна отдельная строка подключения.

import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./rootReducer";

export const store = configureStore({
  reducer: rootReducer,
});

export const fetchUser = (id: string) => async (dispatch, getState) => {
  dispatch({ type: "user/requested" });

  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      throw new Error("User request failed");
    }

    const user = await response.json();
    dispatch({ type: "user/loaded", payload: user });
  } catch (error) {
    dispatch({ type: "user/failed", error: String(error) });
  }
};

В legacy Redux подключение обычно показывают через applyMiddleware.

import { createStore, applyMiddleware } from "redux";
import { thunk } from "redux-thunk";
import rootReducer from "./rootReducer";

const store = createStore(rootReducer, applyMiddleware(thunk));

Практический риск простой. Если thunk middleware нет, dispatch(fetchUser(id)) отдаст в Redux функцию, а не plain action. Без middleware Redux не знает, что с ней делать. Без обработки response.ok, ошибки и сброса loading такой thunk еще и оставит интерфейс в неверном состоянии.

Как подключить Saga

Saga подключается тоже через middleware, но на этом настройка не заканчивается. Вы создаете middleware, добавляете его в store, а потом запускаете root saga. Root saga обычно собирает watcher-саги, которые слушают actions.

import { configureStore } from "@reduxjs/toolkit";
import createSagaMiddleware from "redux-saga";
import rootReducer from "./rootReducer";
import { rootSaga } from "./rootSaga";

const sagaMiddleware = createSagaMiddleware();

export const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(sagaMiddleware),
});

sagaMiddleware.run(rootSaga);

Если вы хотите использовать Saga как основной механизм side effects, можно отдельно решить, оставлять ли thunk в default middleware. На интервью не говорите, что они конфликтуют сами по себе. Они могут жить вместе. Просто смешивание подходов должно быть осознанным.

Thunk
  1. 1Создать store через `configureStore` или legacy `createStore`.
  2. 2Оставить default middleware в Redux Toolkit или добавить `thunk` в `applyMiddleware`.
  3. 3Писать action creators, которые возвращают функцию.
  4. 4Внутри функции вызвать `dispatch`, `getState` и обработать ошибки.
Saga
  1. 1Создать `sagaMiddleware` через `createSagaMiddleware()`.
  2. 2Добавить его в middleware store.
  3. 3Создать root saga с watcher-сагами.
  4. 4После создания store вызвать `sagaMiddleware.run(rootSaga)`.

Как выбрать подход в ответе

Вопрос формально про подключение, но часто за ним проверяют практическое понимание. Не ограничивайтесь фразой про middleware. Добавьте, почему в одном проекте выбирают Thunk, а в другом Saga.

Thunk хорошо подходит, когда логика помещается рядом с async action: показать loader, сделать запрос, обработать успех и ошибку. Но даже в thunk нужно думать о повторном клике, отмене запроса при уходе со страницы и позднем ответе сервера. Saga полезнее, когда side effects похожи на отдельный процесс: нужно отменять предыдущий запрос, ждать несколько событий, запускать фоновые задачи, делать retry или описывать race condition.

Как объяснить выбор на интервью

1Нужно сделать один запрос и обновить состояние?
Обычно достаточно Thunk или `createAsyncThunk`.
2Нужны отмена старых задач, debounce, race или параллельные процессы?
Saga дает более явную модель управления side effects.
3Проект уже на Redux Toolkit и данные идут с сервера?
Сначала проверьте RTK Query. Он может убрать ручные thunk и sagas для API-кеша.
4Команда уже поддерживает один подход?
Лучше продолжить его, если нет явной боли. Смешивание усложняет поддержку.

Ловушка для UI: поздний ответ

Плохой пример: пользователь быстро меняет фильтр или вводит текст поиска, а каждый dispatch запускает новый запрос. Если старый запрос завершится позже нового, он может перезаписать актуальные данные.

const searchUsers = (query: string) => async (dispatch) => {
  dispatch({ type: "users/searchStarted", payload: query });
  const response = await fetch(`/api/users?query=${query}`);
  const users = await response.json();
  dispatch({ type: "users/searchLoaded", payload: users });
};

Что сломается: UI покажет результаты не для последнего ввода, loader может мигнуть несколько раз, аналитика получит лишние события успешной загрузки. Безопаснее хранить requestId и принимать только последний ответ, отменять старый запрос через AbortController или описать поиск в Saga через takeLatest. В production-коде также проверьте response.ok и кодируйте query перед вставкой в URL.

Практический пример ловушки

Плохой пример: saga middleware добавили, но root saga не запустили.

const sagaMiddleware = createSagaMiddleware();

const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(sagaMiddleware),
});

// Плохо: sagaMiddleware.run(rootSaga) забыли

Такой код выглядит почти правильно. Store создается, reducers работают, actions уходят в Redux DevTools. Но watcher-саги не слушают actions, поэтому запросы и бизнес-процессы из sagas не начнутся. В UI это часто выглядит как кнопка, которая включает loader, но данные никогда не приходят, потому что saga с запросом не стартовала. Безопасная формулировка на интервью: Saga подключается как middleware, но ее root saga нужно запустить отдельно.

Что сказать в коротком ответе

Можно ответить так:

Redux Thunk и Redux Saga подключаются к Redux через middleware. В Redux Toolkit thunk обычно уже входит в default middleware, а в старом Redux его добавляют через applyMiddleware(thunk). Saga настраивается через createSagaMiddleware(), затем этот middleware добавляют в store и вызывают sagaMiddleware.run(rootSaga). Thunk позволяет dispatch-ить функции с dispatch и getState, а Saga запускает генераторы, которые слушают actions и управляют side effects.

Если хотите усилить ответ, добавьте практический выбор:

Для простого запроса я бы чаще выбрал thunk или createAsyncThunk. Для процессов с отменой, debounce, race, параллельными задачами или долгими сценариями Saga обычно дает более управляемую модель.

Частые ошибки

Где обычно ошибаются

Проверьте формулировки, которые звучат уверенно, но на интервью быстро выдают пробелы.

  1. 1

    Считать Saga частью reducer

    Saga не хранит состояние и не заменяет reducer. Она слушает actions и выполняет side effects, а состояние все равно меняется через reducer. Если сказать иначе, интервьюер быстро поймет, что вы путаете поток данных Redux.
  2. 2

    Забыть `sagaMiddleware.run`

    Добавить middleware недостаточно. Без sagaMiddleware.run(rootSaga) watcher-саги не стартуют, поэтому запросы, retry и отмена просто не выполнятся. На интервью проговорите оба шага: подключить middleware и запустить root saga.
  3. 3

    Вручную добавлять Thunk в Redux Toolkit без причины

    В configureStore thunk уже включен по умолчанию. Если добавить его повторно или сломать default middleware, можно получить странное поведение логирования, проверок serializable state или лишнюю сложность конфигурации.
  4. 4

    Выбирать Saga для любого API-запроса

    Saga мощная, но добавляет boilerplate и новый уровень абстракции. Для простой загрузки данных thunk или RTK Query часто дают меньше кода и меньше мест для ошибки. Saga оправдана, когда вам реально нужны контроль задач, отмена, race или долгий процесс.
  5. 5

    Не защищать UI от позднего ответа

    В thunk легко забыть try/catch, AbortController или проверку актуального requestId, а в saga легко забыть takeLatest или cancel. Итогом будет зависший loader, потерянный toast или экран, где старый ответ перезаписал новые данные. В ответе сразу скажите, где вы обрабатываете failure, отмену и сброс loading.

Follow-up

Что могут спросить дальше

Короткие ответы на вопросы, которыми проверяют понимание Redux middleware и async flow.

Живые ответы

Видео с похожим вопросом

Если найдем публичные интервью с таким вопросом, добавим их сюда. Их удобно смотреть после теории, чтобы свериться с живыми ответами.

Пока видео нет. Когда появятся подходящие публичные интервью, добавим их в этот блок, чтобы можно было сравнить разбор с тем, как отвечают реальные кандидаты.

Содержание