Эффекты и саги. React + Redux-saga. [Pt. 3]

Я должен признаться, я был не совсем откровенен, называя call или put вспомогательными функциями (и насчет еще некоторых моментов, но забудем пока про это).

call, put и многие другие являются фабриками Эффектов (Effect) – специальных объектов, напоминающих действия (action) из redux, только предназначенные для самой библиотеки saga.

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

Как я уже упомянул, эффекты сильно напоминают собой действия:

{
  CALL: {
    fn: log,
    args: [42],
    context: null
  }
}

Так, к примеру, выглядит возврат из call (немного обрезанный, но тем не менее). Как видно “тип” эффекта прописан ключом объекта.

Впрочем к чему весь этот разговор – эффекты, вспомогательные функции или фабрики, какая разница?

Знакомьтесь, наша новая фабрика take:

function* typing(action) {
    try {
        yield put({type: typingStart});
        yield take(typingStopped);
        yield put({type: typingEnd});
    } catch (e) {
        yield put({type: "Welp, we did it"});
    }
}

После написания этого примера, я начинаю понимать, насколько

В любом случае, фабрика (“вспомогательная функция” мне нравилось больше) take позволяет дождаться действия редакса изнутри обработчика. Применения? Бесконечны! О чем и говорит мой креативный пример.

Как? Мы же в мире генераторов. Исходный код я, конечно же, не смотрел, но предполагаю что saga перенаправляет вызов .next нашего обработчика-генератора на появление указанного действия и “БАМ!” совершенно магическим образом мы получаем это действие внутри обработчика как только оно появляется благодаря dispatch-у.

На самом деле это очень удобный инструмент для поддержания нашей иллюзии синхронности – мы можем взять любое действие – то же начало ввода пользователем – сделать что-то – изменить интерфейс, к примеру – и продолжить выполнение саги уже после другого действия. Более привычный подход заключается в разделении логики на два обработчика – вход в состояние по первому действию и выход из него по второму (более сложная десятистраничная логика приветствуется с распростертыми объятиями).

Однако, текущий вариант не слишком отличается от классического подхода и достаточно бесполезен в чистом виде, но это не конец. saga позволяет помимо прочего, использовать элементы “многопоточного” программирования – fork или race, помимо некоторых других.

Мы даже уже использовали all, который позволяет дождаться выполнения всех эффектов (обещаний) и получить все результаты в один массив. В saga есть его близкий родственник – race. Они похожи, только вот race ждет завершения любого из эффектов и возвращает объект с именованным результатом:

function* typing(action) {
try {
    while (true){
        yield take(textUpdated);
        yield put({type: typingStarted});
        while (true){
            const {updated, timeouted} = yield race({
                updated: take(textUpdated),
                timeouted: call(delay, 1000),
            })
            if (timeouted) {
                yield put({type: typingEnded});
                break;
            }
        }
    }
} catch (e) {
    yield put({type: "Welp, we did it"});
}
}

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

Первый цикл нужен для прослушивания действия textUpdated. Как только мы его получили, можно вступить во второй цикл, перед которым мы отправляем в редакс сообщение, что пользователь начал печатать. Внутри мы создаем условие гонки – или пользователь продолжает печатать – updated– и мы ничего не делая перезапуская внутренний цикл, или проходит 1 секунда – delay(1000[ms]) – и мы отправляем сообщение, что пользователь перестал печатать и выходим во внешний цикл прослушивания.

Почему это лучше?

Несколько причин:

  1. Нам больше не нужно такое в компоненте:
clearTimeout(this.typingTimeout);
this.typingTimeout = setTimeout(() => typingStopped(), 1000);

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

  1. Код отвечающий за состояние печатания выглядит проще (за исключением двух циклов while(true) которые все же немного портят картину) и находится в одном единственном месте.

  2. Нам не нужна еще одна библиотека чтобы сделать задержку или отложенное действие. К примеру, если использовать all вместо race то в результате у нас получится массив со всеми собранными за секунду действиями – очень напоминает буфер из RxJS и подобных. Комбинируя несколько простых эффектов можно получить неплохой набор часто используемых инструментов.

Почему нет?

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

На этом, я думаю, наш небольшой цикл уроков по redux-saga завершается. Осталось пару фабрик и вспомогательных функций, которые мы не разобрали, вроде fork, join или cancel, однако они имеют схожее поведение и очень похожи на свои аналоги из мира многопоточного программирования. Если нужда в их разборе появится, то мы несомненно этим займемся.

Опять же – код доступен в репоа документация по сагам на их сайте.

nil commento load