Pull to refresh

Comments 56

Кстати, отличное замечание! Ведь AbortContoller есть не только как часть fetch или axios (о чем упомянуто в статье), а и как вполне самостоятельная единица. С другой стороны, если его использовать самостоятельно (на в паре с fetch/axios), по сути этот тот же булевский флаг.

Все же AbortController немного по другому действует, если посмотрите в консоли браузера - controller.abort() отменяет запрос к бэкэнду, а то время как реализация с флагом просто игнорирует его. Я к тому что использование AbortController может позволить бэку более эффективно использовать свои ресурсы (ну и траффик между фронтом и бэком немного сэкономим). Ну может это было очевидно, просто я недавно как раз решал похожую задачу с реализацией автокомплита в инпуте и там это было важно

Так я же говорю:

Решения могут быть через AbortController (в случае fetch или в случае использования axios), но достаточно реализовать самый простой способ - с помощью локальной переменной

С точки зрения понимания React мне не так важно как именно будет реализована отмена, важнее что человек понимает почему она нужна (гонки), какими средствами React ("отмена" эффекта) она может быть реализована.

Безусловно, широкий кругозор соискателя (знание AbortController) будет в плюс, но достаточно простого решения.

Кстати, на тему "отмены" запроса к бэку. Совершенно верно, использование правильного механизма отмены дает больше возможностей "сэкономить". Но, с точки зрения запроса от клиента к серверу, его нельзя "отменить". Можно только разорвать соединение. А уж как этот разрыв соединения будет обработан серверной стороной, это уже зависит от конкретных используемых инструментов на бэке и какая именно задача выполнялась (запрос к базе, чтение файла и т.п.).

Как минимум понимание, зачем нужна отмена запроса к беку - это следствие осознания, что на фронте все не заканчивается, а только начинается.
Отмена ненужных запросов очень критична для бекенда. Бек не знает, нужен результат метода или уже нет. Его попросили - он сделал. Но запросы бывают тяжёлыми. Остаётся только полагаться на фронт: чтоб сказал явно, если уже что-то не нужно.
Условному соискателю-фронту потом с банком работать. А если он потом будет выдавать "я сделал у себя, как надо, это всё бек", то возникнут вопросы к лиду.

А как это "сказал явно"? С точки зрения протокола HTTP как можно "явно" отменить запрос?

Вот. Правильный вопрос для собеседования. Идёт после вопроса "а зачем вообще отменять". И перед вопросом "какие есть варианты имплементации?"

Я конечно извиняюсь за душнилство, но в java-script, то есть в React по сути, не может быть race condition, так как он однопоточен. Ваша же ссылка об этом и говорит. Это можно назвать, особенностями асинхронного взаимодействия, или может есть другое название, я не знаю. Но это не race condition

Конечно, JavaScript однопоточный, но вот приложение или система в целом (JavaScript на клиенте + асинхронный HTTP-транспорт + удаленный сервер) вполне может быть рассмотрена как "многопоточная", где однопоточный источник может породить несколько процессов, которые в рамках всей системы будут выполняться параллельно и вполне себе создавать race condition. Так что при всем уважении, термин я оставлю ;)

Мне кажется тоже, что это не состояние гонки. Состояние гонки это когда параллельно какие то данные обновляются/читаются, причем могут выйти непредвиденные результаты.

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

Это просто уборка за собой, если результат уже не нужен. Причем это хак, с флагом, с тех времён, когда AbortController не был стандартом. Сам пользуюсь, потому, что проще и привык :)

Это именно гонка.

Состояние гонки это когда параллельно какие то данные обновляются/читаются, причем могут выйти непредвиденные результаты.

Именно это тут и происходит. Без обработки смены зависимости (которая приведет к выполнению нового запроса) мы можем получить ровно вот это: непредвиденные результаты. Можно получить ситуацию, что интерфейс покажет данные для старого URL при этом пользователь ожидает другой результат.

Многие помнят что "отмена" эффекта нужна при размонтировании компонента. Но многие же забывают, что "отмена" эффекта происходит также и при смене значения зависимостей.

Так вот именно что параллельно, в js ничего параллельного быть не может, сначала отработает один запрос, потом второй, друг за другом, а не параллельно, вот только неизвестно, в каком порядке они отработают. Это уже на сервере они могут выполняться параллельно, и там может быть race condition, но не на клиенте

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

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

Ну, это да, речь просто про React, мне казалось, че там не сервере происходит мы не рассматриваем. В контексте реакта это не гонка

В определении race condition нет ни слова про потоки.

Открываем ссылку из статьи автора, и в первом же предложении там про многопоточность

async/await позволяет организовать несколько логических потоков поверх одного физического, у которых может возникать гонка за общие ресурсы.

Если сказать, что race condition это исключительно про физические потоки, то придётся придумать термин для описания гонки между логическими потоками, например logical race condition. Но пользы для народного хозяйства в этом отдельном термине не много.

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

Извините, за, может быть, наивный вопрос: а что плохого в том чтобы "обмазать результат хука async/await"?))

А как нам это поможет? Вернем из хука Promise? Ну допустим, давай попробуем...

const result = useFetch(url); // result - Promise

Что дальше? Напишем await?

const data = await result; 

Допустим, но тогда внешняя функция должна быть async?

async function MyComponent() {
  const result = useFetch(url);
  const data = await result;
  return <>{data}</>
}

Но в этом случае функция MyComponent возвращает что? Правильно, Promise. А можно из React-компонента вернуть Promise? Как это будет работать?

Теперь ясно, спасибо. Я изначально подумал об оборачивании феча в async await и соответственно try catch)

Здравствуйте)

Мне кажется, что еще можно сделать такие улучшения.

1. Изначально задать isLoading в true. Ну или сделать проверку, что если url есть, то true, иначе false, но это если мы допускаем, что url может быть пустым.
В таком случае мы избавимся от одного лишнего ререндера когда isLoading, при первом вызове, будет становится в true. Но тогда еще и проверку внутри useEffect можно делать, чтобы не делать лишних действий если url не правильный/его в принципе не указали.

2. Я делал еще так, что в цепочке промисов - я не сразу обновляю стейт, а сохраняю в переменные внутри useEffect, чтобы избежать лишних рендеров. А в конце в finally устанавливаю всё сразу. В конкретно этом примере, опять же, это не нужно, но в более сложных - может понадобиться. Например если мы каким-то асинхронным валидатором решим проверить наши данные о.О ну условно) Или банально делаем несколько последовательных запросов или асинхронных операций)

function useFetch(url) {
   const [data, setData] = useState(null);
   const [isLoading, setIsLoading] = useState(true);
   const [error, setError] = useState(null);

   useEffect(() => {
      // флаг отмены
      let cancelled = false;
      let data = null;
      let error = null;

      setIsLoading(true);
      setData(null);
      setError(null);
      fetch(url)
         .then((res) => res.json())
         .then((respData) => {
            if (!cancelled) data = respData;
         })
         .catch((e) => {
            if (!cancelled) error = e;
         })
         .finally(() => {
            if (!cancelled) {
              setData(data);
              setError(error);
              setIsLoading(false);
            }
         });

      return () => {
         // выставим признак того, что запрос отменен
         cancelled = true;
      };
   }, [url]);

   return [data, isLoading, error];
}


Это что касается именно реакта.

Так же - конечно, лучше, как уже писали, и мне так тоже кажется, использовать AbortController.
Так же - я бы использовал не массив для возвращения, а объект, чтобы брать только то что нужно.
Так же - кеширование fetch, или, например, хранить кеши внутри хука.. если нужно.
В общем от задачи хука много зависит) Думаю так.

По поводу объекта и минимизации перерисовок - это отличное замечание, спасибо!

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

Или просто использовать TanStack Query

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

Не надо использовать объект, это зло. С массивом вам тоже никто не запрещает брать только то, что нужно:

const [myData] = useFetch(...);
const [anotherData,, error] = useFetch(...);
const [, loading] = useFetch(...);

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

const { data: myData } = useFetch(...);
const { data: anotherData, error } = useFetch(...);

Субъективно, но кажется такие вот пустые запятые выглядят отвратительно ( Но про переименование поинт хороший.

А можно поподробнее почему объект зло? Ну кроме переименований.

Собственно, этим. Неудобство использования с множественными хуками. Тот же useState не случайно возвращает массив, а не что-то вроде {state, setState}. Представьте, как выглядел бы код, будь оно так.

В остальном, никто не мешает использовать объекты, особенно, если есть уверенность, что таковой хук будет использовать только единожды в компоненте. Например, значение возвращаемое useContext.

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

Объект же даёт вам подсказку в ИДЕ и избавляет от необходимости вешать запятые в воздухе.

Если у вас есть IDE, то она подскажет порядок и для массивов. Если вы используете TS, то там еще проще - можно указать название каждого элемента.

Меня лично не напрягают висящие запятые, они не мешают чтению кода.

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

Эта задача проверяет исключительно понимание реализации на функциональных компонентах Реакта с использованием хуков. Для проверки этого можно придумать пример порелевантней, потому что если использовать кейс получения данных по апи - то адекватный ответ для всех, кроме джунов, это - "так делать нельзя". А джуны как раз будут пилить setState, useEffect, обрабатывать ошибки и бороться с race condition) То есть как раз "уверенно владеющие основами" люди. Если надо найти именно таких - то статья релевантная, хотя лучше бы без вызова апи, потому что отсекает более опытных разрабов.

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

  • как организовать апи, чтобы можно было вызывать как из Реакта, так и из экшенов / моделей / реакций, и при этом в Реакте можно было узнать состояние запроса и отобразить лоадер

  • как сделать изоморфные вызовы во всех этих кейсах, чтобы SSR дожидался выполнения всех вызовов из всех мест и показывал финальный html пользователю, при этом при ошибке критичных запросов редиректил на страницу 500 или применял другую стратегию

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

  • как тестировать апи с моками и отдельно от вью/моделей и других слоев, а также в связке с ними

  • как сделать реалтаймовую валидацию входящих-исходящих данных исходя из TS-моделей

  • как связать отображение ошибок и глобальные компоненты (нотификации, модалки), при этом оставляя возможность отобразить ошибку в глубоко дочернем компоненте (например, вывод ошибки красным текстом под полем + нотификацию на странице в верхнем правом углу)

И т.п. То есть вопрос про вызов апи - это отдельная большая тема. А вопрос про хуки Реакта и асинхронщину внутри них - совсем другая. И если вопрос про апи, то там наиболее адекватно было бы сказать, что хуки для этого не подходят, нужно использовать классовые компоненты, что приведет к отдельной ветке разговора)

Со всем согласен, кроме "отсекания более опытных разрабов". Можно использовать задачу как отправную точку. Ответ "так нельзя" это отличный повод задать вопрос "а почему?" и продолжить разговор, выслушать все, что описано выше.

Да, как отправную точку - отлично, но в выводе статьи - "Задачка позволяет проверить, как кандидат понимает устройство рендеринга React...", то есть ответ "так нельзя" скорее всего приведет к тому, что интервьюер либо решит, что задачу кандидат не решил и не знает основ, либо не сможет проверить эти знания потому что под рукой нет более подходящей задачи. То есть все же считаю что правильно сказал - определенные разрабы отсекутся этой задачей, потому что она проверяет для интервьюера одно, а для соискателя может быть совсем другое (знание архитектур, а не setState и useEffect Реакта)

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

Так в том и состоит задача интервьюера что бы постараться раскрыть кандидата при любых раскладах. Понятно что не надо по одной задачке судить всех.

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

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

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

И такие найдутся, которые будут считать что это задача на хуки, а кандидат думать что на апи - и разойдутся.

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

Хороший кандидат задаст вопрос - что хотим тут получить? Уточнит требования, задаст вопросы о валидации, изоморфности и т.п. если это позволяет его кругозор. Хороший интервьюер направит в нужное русло и не будет слепо следовать "скрипту", сравнивать решение кандидата построчно с кодом из статьи на Хабре, верно же? )

Все так, для статьи "любимая задача про знание Реакт" придраться больше и не к чему, кроме как к тому, что интервьюеры "могут не обладать соответствующими знаниями". Но вот к "Вот такой вариант я считаю "идеальным"" придраться можно очень много к чему) потому что код в статье - это решение джуна, неприменимое к реальности

А что не так с кодом? А что в нем поменяет с ростом уровня "сеньйорности" собеседуемого? Уточню - это не тестовое задание "на дом". Это задачка "на вайтборде", здесь и сейчас, минут на 10-15, не больше.

Ее основная цель (повторю в который раз) - проверить понимание основ React.

Конечно же она НЕ используется как единственный критерий оценки.

Ну вот позвольте рассказать, как я бы воспринял эту задачу, чтобы объяснить мою позицию.
Я работал в десятке компаний и сделал больше сотни сайтов, половина - на Реакте. Каждый день вырабатывал практики, как организовать слои приложения так, чтобы они были одновременно независимыми и при этом легко интегрировались друг в друга.
Я ожидаю, что на собеседовании проверят не то, как я различаю == и ===, и не чему будет равно [] == true, а проверят реальный опыт на реальных задачах, чтобы я смог показать глубину опыта.
Мне дают на собеседовании задачу про "компонент получает url", на что я сразу напрягаюсь, "компонент функциональный", на что соответственно сразу думаю про SSR, "хук, которых фетчит данные", на что сразу думаю - куда я попал?
Первое, что я отвечу - так делать нельзя и только джуны могут так делать. Расскажу про все, что перечислил в первом посте, займет минут 20. У собеседующего время ограничено, он не проверил мое "знание хуков Реакта", но получил море как бы не связанной с изначальным намерением информации.

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

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

В итоге - мы оба потратили много времени на обсуждение организации апи и по итогу часа я выполнил 1 задачу из 3. Собеседующий так и записал, пришло письмо от HR "вы нам не подходите". История не особо выдуманная, на опыте было немало такого) Может, это мои заморочки, что не готов обсуждать совсем базовые штуки (шутка про Яндекс, в который я прошел около 20 собесов в разное время, чтобы увидеть какая дичь у них в коде), но и на джуна я не собеседуюсь - а такие вопросы тем не менее в каждой первой компании.

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

А с каких пор useQuery() из apollo или react-query стали не подходить?!

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

А что должно быть?

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

Должен быть контект для решения задачи. "Наш продукт имеет систему виджетов, которые могут получать данные из разных источников, поэтому рассмотрим вариант когда url для фетча передается в компонент из внешнего источника". Дальше - "допустим, вы решили делать фетчинг через кастомный хук, расскажите, какие минусы и плюсы у этого решения и как бы вы это реализовали". Вот это была бы достойная задача на собеседование, проверяющая глубину опыта. А без контекста - это конь в вакууме, проверяющий читали ли вы документацию Реакта и fetch, чтобы проверить самую базовую базу

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

Почему плохо - я привел несколько аргументов в комментарии выше.
Про сильную связанность с view, про сложность с SSR, про невозможность вызова вне Реакта (например до рендеринга приложения или в ответ на какую-то реакцию), про сложности генерации реалтаймовых валидаторов, про отсутствие возможности протестировать вне компонентов, про слабую связь со стором (например для вывода нотификаций).
Как хорошо - делать независимый от фреймворка слой апи, при необходимости передавая в него разные сущности типа сторов, экшенов, локализации. Пример я приводил здесь https://github.com/mobxjs-ru/api-by-config

Этот хук не будет работать как ожидается без проверки статуса ответа

У меня 2 вопроса/замечания:

  1. Вы с помощью таких задач ищете людей с опытом, чтобы они и в проде такое выдавали и считали это нормой?

  2. Вы уверены, что правильно понимаете понятие race condition? Это не риторический вопрос, т.к. если мы запросили 2 разных url через этот хук, то это не состояние гонки. Гонка это когда у вас 2 одинаковых запроса идут и кто первый ответил, тот и молодец))) А у вас по коду получается, что компонент при монтировании/размонтировании просто для вида локальную переменную поменяет и тут же вычистит. Запрос при этом может уйти на сервер, исполнится там с 200 OK и, например, в случае с JWT, обновит токен с ответом вникуда, что повлечёт следом некорректную работу на фронте (невалидный токен).

  1. Вы с помощью таких задач ищете людей с опытом, чтобы они и в проде такое выдавали и считали это нормой?

Еще раз повторю то, что указано в начале поста. Это всего лишь одна из задач, которые я применяю на собесе. Ее функция - быстро (это не тестовое задание домой, это на условном вайтборде задача, минут на 10-15 с разговорами) оценить понимание основ React.

Вы уверены, что правильно понимаете понятие race condition?

Уверен. Более чем. Это именно состояние гонки. Т.к. без обработки "отмены" тайминг обработки запроса сервером и его RTT будет влиять на результат, который получит пользователь. В том числе мы можем получить что "молодец" как раз не тот, кто ожидался.

А у вас по коду получается, что компонент при монтировании/размонтировании просто для вида локальную переменную поменяет и тут же вычистит

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

Запрос при этом может уйти на сервер, исполнится там с 200 OK и, например, в случае с JWT, обновит токен с ответом вникуда, что повлечёт следом некорректную работу на фронте (невалидный токен).

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

Почему именно задача про загрузку данных? Рассматривали ли вы задачи связанные с context и его особенности ре-рендера? Сколько не посещал таких мероприятий почти всегда было "напишите ка нам хук, где нужно данные получить"

Потому что там асинхронность. Это сразу открывает много проблем, о которых можно поговорить и понимание которых проверить.

" Если url изменится, то запроса данных не произойдет. " может вы имели в виду "если url НЕ изменится"? А если я не прав с поправкой, то можете объяснить плиз почему так произойдет?

В тексте все верно. Если у эффекта не указана зависимость (url), то при смене url эффект не пересчитается, новый url загружен не будет. Т.е. хук будет загружать только самый первый адрес после монтирования компонента.

Для проверки основ и начала диалога норм

Sign up to leave a comment.