Как стать автором
Обновить

Комментарии 33

в итоге я попался на блоке finally или ничего страшного не произойдет ?

Попался, потому что в таком случае вызов 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, что конечно же, недопустимо. Поправлю это в статье

спасибо

Вот как вот в JS умудрились сделать await-ы хуже, чем получается сделать то же на yield? И зачем вообще этот async делали, если сразу было понятно, что на yield можно запилить любые монадки?

Сначала был колбек-хелл, потом промис-чейн-хелл, потом недолго поиграли в генераторы и ушли в асинк-авейты. Сейчас, видимо, процесс назад. Вернулись к генераторам. Но потом станет понятно что это усложнение туда-сюда вызовы гонять и можно же цепочки и там выходить явно. А ещё более явно это непосредственно вызовы функций, тем более чистых функций, и не такой уж и плохой был колбек-хелл. Классы уже тоже кое-где всё, вернется мой дветыщиседьмой и ES3/ES5. Да и всплывающий var и function декларейшн своё зерно рационализма имеют.

А если серьезно, то можно ведь после завершения await проверять нужно ли оно дальше и в целом предложенный инструмент даёт свои плюшки, выглядит как вариант решения, но не единственный. Впрочем - авторы молодцы.

Потому что функции не чистые и любые монадки кроме одобренных партией будут легко крошиться.

Ну так асинк эвейты это специализированное решение для конкретного кейса, и в рамках этого кейса они лучше генераторов.

>если сразу было понятно, что на yield можно запилить любые монадки?

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

@insecto

>Потому что функции не чистые и любые монадки кроме одобренных партией будут легко крошиться.

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

>асинк эвейты это специализированное решение для конкретного кейса, и в рамках этого кейса они лучше генераторов.

Как минимум, await всегда откладывает продолжение на следующий тик, даже если вернуть что-то синхронно. А это было бы полезно, скажем, на беке - чтобы задачу, которая часто может выполниться целиком синхронно (достать все из кешей, например) - не кидать в конец очереди, увеличивая latency, и держа лишнее время все нужные ей ресурсы.

Интересно было бы, кстати, посмотреть на тесты перформанса для этой Effection. А то может оно еще и быстрее работает, чем нативные промисы - как раз за счет этого.

В 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);

Спасибо, что есть RxJs, которому уже почти 10 лет. Все уже давно придумано, но в реакте до сих пор изобретают велосипеды

В точку!

Причем здесь State Management и RxJs? В статье речь об отмене потока и аналогу async|await, с чем RxJs справляется на ура через модель Subscription
А в плане страшности, цепочка из пары функций в .pipe() выглядят в разы читабельнее, чем этот ужас сверху с yield

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

fromEvent(this.myInput.nativeElement, 'keyup')
.pipe(
  // задержка ввода 200ms
  debounceTime(200),
  map(value => value?.trim().toLowerCase()),
  // проверка, что старое значение не равно новому
  distinctUntilChanged(),
  // switchMap сам отменит асинк операцию, если поступит новое значение до того, как цепочка действий завершится
  // from() - конвертирует async в Observable<>
  switchMap(value => from(axios.get(`myapi.com/news?query=${value}`)))
).subscribe(result => результаты автозаполнения)

итого,за 6 строк мы сделали - подписку на ввод, задержку нажатий, трим и приведение регистра, автоматическую отмену Get запроса
вдобавок еще и проверку на то, что новый ввод отличен от старого, чтобы просто так не трогать сервер

где здесь "страшен как атомная война " ?

единственное, что я упустил - отписку от keyup, но это еще +1 строка, в зависимости от подхода

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

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

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

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

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

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

где нужно выставлять индикатор загрузки? [...] Но насколько правильно менять стейт внутри pipe? Судя по комментарию из твоего кода, результаты автозаполнения задаются в subscribe. Т.е. в моем понимании нужно сделать еще один subscribe, а потом снова иметь pipe.

Вот чего точно не надо делать - так это pipe после subscribe. Это просто не будет работать, в силу ошибки типизации.

Менять состояние внутри pipe - совершенно нормально (до тех пор пока вы понимаете что делаете), для этих целей даже есть оператор tap (хотя конкретно тут он не нужен).

Сам индикатор загрузки нужно ставить там же, где и делается загрузка - внутри switchMap:

switchMap(value => concat(
  defer(() => { /* загрузка началась */; return [] }),
  from(axios.get(`myapi.com/news?query=${value}`))),
  defer(() => { /* загрузка закончилась */; return [] })
))

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

Ну, потому ошибки и нужно, ну, обрабатывать. Так же как и в обычном коде. Да, это будет ещё один pipe внутри switchMap, что увеличит число строчек.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Жду ссылки :)

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

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

Жду ссылки :)

Могу разве что ссылку на документацию привести. Я уже давно не делал ничего на rx.js и не буду возвращаться во фронтенд ради комментария на Хабре.

но это никак не мешает использовать axios или какой-нибудь graphQL

Так и rx.js не мешает ничего использовать, просто нужна подходящая обёртка. Для fetch есть даже встроенная - fromFetch

где нужно выставлять индикатор загрузки

сразу же первым оператором в цепочке можно

.pipe( 
  tap(() => setLoading(true))
  // выполнится после закрытия потока при любом условии success|error
  finalize(() => setLoading(false))
... далее из примера
)

каким образом происходит обработка ошибок

.pipe( 
  // здесь или делаем заглушку, например
  // или прокидываем новую ошибку throw new Error('text')
  catchError(() => return of(null))
... далее из примера
)
// либо ловим ошибку внутри .subscribe()
.subscribe(
    (data) => this.onSuccess(data),
    (error) => this.handleError(error)
);

как принудительно отменить всю цепочку

присвоить весь поток переменной типа new Subscription() и потом вызвать myVar.unsubscribe()

Как действительно отменить http запрос в этой цепочке

все уже сделано из коробки, запрос сам уйдет в Cancelled, нужно просто пользоваться http сервисом от rxjs, как отметили в комментарии выше, в ангуларе он уже встроен

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

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

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

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

Просто здесь комплексное решение любой задачи, связанной с асинхронностью, от этого и удобство)
Можно как поймать событие, так и обратиться к серверу, так и сохранить состояние с мемоизацией и многое многое.
Согласен, что, когда работаешь с одним инструментом, то привыкаешь к нему, в ангуларе общий подход немного другой - там все есть из коробки, специально для ентепрайза - запустил и работаешь.
В реакте же собирается конструктор по кусочкам, чтобы можно было выполнить текущую задачу, оттуда и бесконечный набор библиотек под каждую из них.
Кому как удобнее - уже каждый выбирает сам

Здесь три кастомных оператора и парочка стандартных на реализацию дебаунса. А заодно вопросы вроде: "Куда втыкать breakpoint'ы при отладке?", "Куда вставлять setLoading?", "Куда вставлять cancellationToken, если вдруг нужно будет отменять запрос?".

Всё это объясняет почему в Angular при встроенном RxJS, тащили Redux, а сейчас наконец добавили нормальную реактивность на атомах.

О каких кастомных операторах идет речь? все доступно из коробки после npm i

Куда вставлять setLoading

ответил сообщением выше

Куда втыкать breakpoint'ы при отладк

в любой оператор или добавлять .tap(data => console.log(data)) между любыми операторами цепочки и смотрим результат

Куда вставлять cancellationToken

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

Всё это объясняет почему в Angular при встроенном RxJS, тащили Redux, а сейчас наконец добавили нормальную реактивность на атомах.

не понимаю, о чем речь)
там NgRx уже как 9 лет существует
единственные изменения, которые происходили - с последних версий добавили Signals
и то, только для того, чтобы выйти на поддержку ZoneLess приложений и уйти от обертки, когда происходит много лишний действий с переотрисовкой и обновлением состояния, а обновлять ее руками, только там, где нужно

Неплохая альтернатива Redux-Saga, особенно если их действительно удобно использовать точечно.

Почему точечно? Генераторное API имеет преимущество перед async-await в поддержке отменяемости, но из-за этого же теряет в читабельности. Собственно код async-await с CancelationToken тоже выглядить так себе. Потому что отменяемость процесса приводит к тому что код не выглядит линейным, его становится тяжело читать.

В реальности отменяемость асинхронных запросов и цепочек действий нужна очень редко.

То есть инструменты вроде Effector - это не для встраивания в компонент React. Это для встраивания во всякие библиотечный функции/хуки. Которые или пишутся в паре мест, нормально покрываются тестами и комментариями, или вовсе импортируются готовыми. В обоих случаях генераторный API будет приватным и за пределами модуля о нём никто знать не будет.

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

import useSWR from "swr";
import { useDebounce } from 'use-debounce';
import { fetcher } from "@api/utils" // const fetcher = (url) => fetch(url).then((res) => res.json());

const Autosuggest = () => {
	const [input, setInput] = useState('');
	const debouncedSearch = useDebounced(search, 300);
    const { data, error, isLoading } = useSWR(
      `/search?name=${debouncedSearch}`,
      fetcher
    );

	const onChange = async (e) => {
		const value = e.target.value;
		setInput(value);
	}

	return (
		<div>
			<input type="text" value={input} onChange={onChange}/>
			{data.map(res => (
				<div key={res.id}>{res.name}</div>
			))}
		</div>
	);
}

И по-моему такой код по простоте уже ничем не перебить, не теряя в функционале.

А вот внутри useSWR либо вашей какой-то библиотечной функции, вроде подключения к сокету, Effector вполне может оказаться нужон.

И здесь его возможность standalone работы — это большой плюс.

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

  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 не может отменять запросы и перезапрашивать данные с таким же параметром. Это может хорошо подойти, например, для детального вида, но в случае с обработкой действий пользователя уже является ограничениями.

У вас и так происходит перерендеринг из-за ввода в input на каждый символ. Вся разница в отдельном вызове setDebounceValue и setLoading. Лишние useEffect в обработчкие - это действительно плохо, но тут он ровно один и на внешний вызов setTimeout. Если вам нужно действительно сэкономить рендеры, то скорее всего сделают отдельный DebouncedInput.

useSWR  конечно отменит запрос, если параметр изменился и не будет ничего делать, если параметр поменялся. Зачем может потребоваться другое поведение - не представляю.

Но повторюсь. Если уж очень нужен свой useDebouncedQuery, то Effection может и хорошее решение.

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

А, вы про принудительное обновление по клику. Есть конечно.

Хм, складывается ощущение, что генераторы сильно упрощают код по сравнению с async/await. Но это неправда. Та же библиотека Effection прячет все операции под ковер (то есть в свой модуль), поэтому так все аккуратно выглядит. Я к тому, что можно писать довольно аккуратный код и с async/await. Тут у вас примеры под реакт, в свою очередь, я могу написать пример кода под vanilla js, если кому-то интересно. Можете даже накидать задач со сложной логикой.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории