Pull to refresh
9
20.1
Send message

Спасибо. Возможно код выше для кого-то и выглядит привычнее, но возвращаясь к самому первому комментарию в ветке, я вижу ровно наоборот: rxjs пытается решить проблемы, которые давно уже решены.

Безусловно rxjs хорош в потоках и управлении ими, но в данном примере у нас только компонент autosuggest и обработка действий пользователя. При попытке сравнить это с потоком выходит гораздо больше сложностей (tap, finalize, pipe, subscribe, switchMap, rxjsClient).

В случае с моим примером на генераторах можно вообще ничего не знать об effection (в логике обработки данных есть только sleep из него), т.е. имея базовые знания о js и генераторах уже можно прочитать и написать похожий компонент.

Все таки хочется применять инструменты там, где они принесут пользу, а не просто потому, что мы их знаем (Это справедливо также и для effection и React в целом)

Подписки не может не быть, без подписки большинство операторов в пайпе просто не будут работать (они слишком "ленивые").

Жду ссылки :)

Как будто axios.get - нативная конструкция :-)

В статье я использую fetch (+есть пример с WebSocket), но это никак не мешает использовать axios или какой-нибудь graphQL :)

Было бы хорошо, если бы был полный пример, можно ссылкой на gist.

Менять состояние внутри pipe - совершенно нормально. Через dispose подписки, возвращённой методом subscribe. Хотя очистку я бы делал по-другому.

А как отписываться, если подписки не было (все внутри pipe)?

Отказаться от from(axios.get и использовать поддерживающий rx.js клиент.

Похоже, что rxjs заставляет избавиться от множества привычных нативных конструкций

Перезапросить данные можно, например, при повторном нажатии на кнопку поиск или по клавише Enter. В репозитории, который я приложил к статье, как раз есть пример autosuggest с пагинацией и подвертждением https://github.com/Atrue/react-concurrency-examples/tree/main/src/examples

Спасибо. У меня несколько вопросов:

  • где нужно выставлять индикатор загрузки? Т.к. в наших примерах он ставится после дебаунса, то логически его нужно вставить между debounceTime и map. Но насколько правильно менять стейт внутри pipe? Судя по комментарию из твоего кода, результаты автозаполнения задаются в subscribe. Т.е. в моем понимании нужно сделать еще один subscribe, а потом снова иметь pipe.

  • каким образом происходит обработка ошибок? Насколько я помню, в rxjs есть момент, что после ошибок поток закрывается.

  • как принудительно отменить всю цепочку? Например, если пользователь нажал кнопку "очистить" в поле для ввода или ушел с этого компонента.

  • не особо критично, но все же. Как действительно отменить http запрос в этой цепочке? В этом случае все же switchMap просто отбрасывает результат промиса, но сам запрос и промис все равно будут выполнятся до конца.

Возможно после этого строк будет уже больше. Хотя даже сейчас уже можно сравнить с кодом на генераторах из раздела ## Генераторы + effection (10 строк включая try/catch). Ужасом я ни то, ни другое не хочу назвать, но при прочих равных, кажется, что декларативный стиль с rxjs тут ничем не выигрывает у привычного императивного с yield. Плюс это дополнительные проблемы с дебагом. Тут, допустим, все относительно несложно, но в сложной логике переход между конструкциями в дебаггере внутри pipe может доставить боль.

Было бы хорошо сравнить, если напишешь тот же самый пример на RxJs (желательно на React)

Спасибо за комментарий. Хотелось бы обсудить несколько моментов:

  1. В реальности отменяемость асинхронных запросов и цепочек действий нужна очень редко. Это очень спорный момент, тут скорее можно сказать, что об отмене задумываются очень редко, из-за чего можно словить разного рода баги. Я нередко ловлю баги связанные с браузерной навигацией (вперед\назад). Я пользуюсь ей довольно часто (у меня эти кнопки есть на мышке). Ситуации бывают разные, лучше смотреть на конкретный пример. В случае с autosuggest - это стоит делать.

  2. но из-за этого же теряет в читабельности. В статье я хотел донести, что читаемость не теряется (возможно, если только стоит привыкнуть к yield* вместо await). Но все остальное - вполне себе линейно, просто и понятно. Можем на конкретном примере обсудить.

  3. То есть инструменты вроде Effector. Может быть опечатка, но в статье я использую Effection. Согласен, запутаться очень легко, есть Effector, Effection, а еще есть useEffect в реакте

  4. В обоих случаях генераторный API будет приватным и за пределами модуля о нём никто знать не будет. Уточню. Апи вроде run/halt/call из effection будут использоваться не так часто, и по хорошему эти вызовы должны быть завернуты в проектные хуки или функции, но сами генераторы могут использоваться в любом месте и легко переиспользуются в других генераторах черезyield*.

  5. В реальном приложении код из примера будет выглядить так: Немного комментариев по поводу кода:

    • Из плюсов: очевидно кода стало меньше

    • Но самый главный минус - код перестает быть линейным. Функция onChange меняет только стейт input. Дальше хук useDebounce меняет еще один стейт debouncedSearch. А дальше эта результат уже используется в другом хуке useSWR. То есть мы пытаемся заменить линейный код на обработчике последовательными вызовами useEffect, которые один за одним меняют промежуточный стейт, вызывая дополнительные ререндеры, из-за чего ухудшается читаемость, производительность и усложняется обработка промежуточных результатов.

    • Насколько я понимаю, useSWR не может отменять запросы и перезапрашивать данные с таким же параметром. Это может хорошо подойти, например, для детального вида, но в случае с обработкой действий пользователя уже является ограничениями.

В Effection есть только конструкция yield*, через которую можно вызвать только генератор. Все, что не является генератором, можно обернуть через call (https://deno.land/x/effection@3.0.3/lib/call.ts?s=Callable и https://deno.land/x/effection@3.0.3/lib/call.ts?s=call). Event loop тут тот же самый, синхронные операции выполняется синхронно, промисы и таймауты выполняются в своей очереди задач

const res1 = yield* call(() => 1); // идентичнно const res = (() => 1)();
const res2 = yield* call(Promise.resolve(5)); // идентичнно const res = await Promise.resolve(5);

Попался, потому что в таком случае вызов setLoading(false) будет происходить каждый раз при отмене запроса. Поэтому порядок вызовов при отмене будет следующий:

// новый запрос
setLoading(true);
// отмена предыдущего запроса
abortRef.current?.abort();
// предыдущий запрос завершается с ошибкой,
// срабатывает условие выхода из catch
// if ((e as Error)?.name === "AbortError") return;
setLoading(false) // из finally

// следующий запрос успешно завершается
setLoading(false);
setData(data);
setError(null);

Т.е. после отмены запроса компонент будет в состоянии loading = false, что конечно же, недопустимо. Поправлю это в статье

Information

Rating
289-th
Registered
Activity

Specialization

Frontend Developer, Fullstack Developer