Я не люблю магию, магией мы часто объясняем какое то поведение части приложения, библиотеки или фреймворка, которое не понимаем, а когда наконец прочитав документацию понимаем, как тот или иной функционал реализован на самом деле, ловим себя на мысли, что никакой магии нет, только грани нашего познания и что на самом деле все просто, когда разберешься.   Так вот как то раз смотря документацию Relay я наткнулся на мысль, что не понимаю, как работает связка Relay.useLazyLoad и React.Suspense. В частности не понятно, как именно React.Suspense понимает, что вот прямо сейчас происходит асинхронный запрос и самое время отрисовывать fallback?  Сначала давайте разберемся что же такое этот ваш Relay, если вы с ним уже знакомы то следующие пару абзацев можно смело пропустить.Как написано на официально сайте, это GraphQL клиент который масштабируется вместе с нами, чтобы это не значило.  Я тут конечно сразу вспомнил знаменитый видосик Web Scale если по каким то причинам вы его еще не видели, то к просмотру обязательно.  В итоге Relay это библиотека для React приложений, которая позволяет удобно работать с GraphQL, берет на себя вопрос с хранением, обновлением и кэшированием сущностей на клиентской стороне, а так же умеет в хитрые оптимизации для реализации паттерна render as you fetch, data-masking и data-colocation.  Немного про Relay.useLazyLoad — это хук, как следует из названия, передаем ему GraphQL query, параметры и получаем данные в ответ. Вроде все понятно, но чего то не хватает, самые внимательные наверняка уже заметили, что перед вызовом Relay.useLazyLoad в примере нету ключевого слова await, так это что синхронный запрос? На самом деле конечно нет, это асинхронный запрос, но обрабатывается он весьма интересным способом.  Если мы посмотрим документацию или прикладной код в примерах не углубляюсь внутрь React и Relay на уровне приложения, мы увидим что явно, напрямую друг с другом мы не передаем информацию о состоянии запрос. Нету никакого return с Promise, или EventEmmiter-а или какой либо еще абстракции, которую мы бы использовали для передачи состояния запрос. Но мы знаем что как то это да работает, магия? Конечно нет, если этого не делаем мы напрямую, значит кто то делает это за нас, а как именно, давайте разбираться. Давайте наконец познакомимся с React.Suspense API, прошу не путать с <React.Suspense />, не знаю почему разработчики не придумали какое-нибудь отдельное название для API, возможно потому что фича еще в experimental статусе, но обещает быть в стандартном наборе инструментов в React 18.  Вместо того чтобы читать инструкцию и документацию, давайте сделаем то что мы с вами любим больше всего, посмотрим на код! Код это ведь и есть набор инструкций! Давайте создадим небольшой пример приложения с использованием React.Suspense API.  App.js входная точка приложения Компонент UserWelcome.js Реализация функции fetchData Реализация функции wrapPromise в которой происходит вся магия. Оставлю ссылку на статью где я взял этот пример, спасибо автору Давайте разбираться что мы тут понаписали. App.js типичная входная точка приложения, ничего особенного интересного в нем нет, разве что стоит заметить, что компонент UserWelcome и Todos отрисовываются внутри React.Suspense, это будет важно дальше. Компонент UserWelcome отрисовывает данные пользователя и для этого делает асинхронный запрос чтобы их получить с помощью API клиента fetchData, который возвращает переменную resource с методом read(), который возвращает данные. fetchData — простая функция утилита которая получает на вход URL запроса, использую нативный window.fetch инициирует запрос данных с сервера, получает в ответ объект Promise, не дожидаясь его выполнения передает в функцию wrapPromise и возвращает результат ее выполнения. wrapPromise — по сути в себе и содержит всю магию Suspense API. Самое интересное в реализации метода read(). В зависимости от текущего статуса promise, который синхронизирован с локальной переменной, мы либо используем throw чтобы выбросить ошибку promise в переменной response, либо throw самого promise, либо возвращаем данные с помощью конструкции return. Давайте по порядку, throw ошибки выглядит логично, разрываем call stack и какой-нибудь <ErrorBoundary /> компонент отрисует состояние ошибки, ничего нового, мы так всегда лелали. А вот throw promise, это что то новенькое, обычно мы используем ключевое слово await и ожидаем возвращение данных, здесь же мы снова ломаем call stack и выбрасываем его "вверх". А кто собственно будет ловить? Ну я думаю вы уже догадались, что внутри компонента React.Suspense реализована логика по типу этой. Естественно это супер упрощенная реализация просто для понимания и представления, в реальности реализация гораздо более сложная и нужно позаботится о сотни edge кейсов Это намеренно упрощенный пример использования и реализации Relay.useLazyLoad Ну в общем то на этом месте статью можно было бы и заканчивать, тайна раскрыта? Но лично меня Suspense API и использование try/catch не только для пробросать исключений заставило задуматься, в javascript мире весьма редко кто то использует try/catch в таком ключе.  Интересное API получается, не очень понятно правда пока зачем? Почему бы не воспользоваться уже существующим функционалом в React, создать state/contex, прокинуть callback. А давайте попробуем реализовать эту логику без Suspense API.  Давайте попробуем  Обновим немного наш App.js Реализация файла OurCustomSuspenseContext.js Обновленный UserWelcome, cсылка на codesandbox, чтобы посмотреть как это чудо работает И так что мы имеем, в App.js добавился новый компонент OurCustomSuspenseProvider, который реализует логику работы с хранением данных, содержит в себе callback с асинхронной логикой и проталкивает дальше контекст.  OurCustomSuspense — наша реализация React.Suspense, который слушает данные из контекста и решает пытаться отрисовать children или fallback. Он содержит в себе локальный state, потому что нам нужно вызвать дерево в первый раз чтобы инициировать асинхронный запрос с помощью callback requestData.  UserWelcome получает данные и callback через контекст, при отсутствии данных вызывает callback.  В целом render-as-you-fetch работает, естественно это достаточно примитивный концепт, в котором поддерживается не все, на что способен Suspense API. Нам нужна явная привязка OurSuspense и вызывающего компонента по hash, это конечно можно масштабировать, но будет не красивое и не очень удобное API. Так же Suspense API не имеет привязки к lifecycle события React компонента, что на сколько я понимаю достаточно важное решение для команды React и позволяет гибче манипулировать деревом состояний.  Давайте подитожим и разберем плюсы Suspense API. Спасибо большое что прочитали статью, поделитесь вашим мнением, как вам идея использования try/catch as control flow, пробовали ли вы Relay, может кто то уже написал свою классную библиотеку для работы с асинхронной логикой с использованием Suspense API?