Задержка при получении данных (экспериментально)

Внимание:

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

Большая часть информации на данной странице уже не актуальна и оставлена для истории. Актуальная информация приведена в посте блога React 18 Alpha announcement.

Перед выходом React 18 информация на этой странице будет обновлена.

В React 16.6 добавлен компонент <Suspense>, который позволяет «ждать» загрузки кода и декларативно показывать состояние загрузки (например, спиннер), пока мы ожидаем:

const ProfilePage = React.lazy(() => import('./ProfilePage')); // Ленивая загрузка

// Показать спиннер, во время загрузки профиля
<Suspense fallback={<Spinner />}>
  <ProfilePage />
</Suspense>

Задержка при получении данных — это новая возможность, которая позволяет использовать <Suspense> и декларативно «ждать» чего-либо ещё, включая данные. Эта страница описывает получение данных, однако это применимо к изображениям, скриптам и другим асинхронным действиям.

Что такое задержка?

Задержка позволяет вашим компонентам «ждать» чего-то до их рендера. В этом примере два компонента ждут асинхронного вызова API, чтобы получить данные:

const resource = fetchProfileData();

function ProfilePage() {
  return (
    <Suspense fallback={<h1>Loading profile...</h1>}>
      <ProfileDetails />
      <Suspense fallback={<h1>Loading posts...</h1>}>
        <ProfileTimeline />
      </Suspense>
    </Suspense>
  );
}

function ProfileDetails() {
  // Пробуем прочитать информацию о пользователе, хотя она может быть ещё не загружена
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

function ProfileTimeline() {
  // Пробуем прочитать сообщения, хотя они могут быть ещё не загружены
  const posts = resource.posts.read();
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}

Посмотреть пример на CodeSandbox

Это раннее демо. Не переживайте, если оно кажется бессмысленным. Мы поговорим о том, как оно работает ниже. Имейте в виду, что задержка — это больше механизм, и определенные API, такие как fetchProfileData() или resource.posts.read() в примере выше не очень важны. Если интересно, вы можете найти их определения прямо в демо песочнице.

Задержка — это не библиотека для получения данных. Это механизм для библиотек получения данных, который сообщает React, что данные, которые читает компонент, ещё не готовы. React в этом случае может подождать, пока они будут готовы и обновить пользовательский интерфейс. В Facebook, мы используем Relay с интеграцией новой задержки. Мы ожидаем, что другие библиотеки, такие как Apollo смогут предоставить подобную интеграцию.

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

Чем задержка не является

Задержка — это совершенно иной подход среди существующих для решения этих проблем, поэтому на первый взгляд это часто ведёт к заблуждениям. Давайте проясним самые распространенные:

  • Это не реализация получения данных. Задержка не предполагает, что вы используете GraphQL, REST или какой-то другой определённый формат данных, библиотеку, транспорт или протокол.
  • Это не готовый к использованию клиент. Вы не сможете заменить fetch или Relay задержкой. Но вы можете использовать библиотеку, в которую интегрирована задержка (например новый API у Relay).
  • Задержка не привязывает получение данных к слою представления. Она помогает управлять отображением состояния загрузки в пользовательском интерфейсе, но она не связывает вашу сетевую логику с React-компонентами.

Что позволяет делать задержка

Итак, в чём идея задержки? Есть несколько вариантов ответа на этот вопрос:

  • Она позволит глубже интегрировать React в библиотеки получения данных. Если библиотека получения данных реализует поддержку задержки, её использование из React-компонентов будет выглядеть естественно.
  • Она позволит вам управлять намеренно спроектированными состояниями загрузки. Она не говорит как данные получены, но позволит вам лучше контролировать визуальную последовательность загрузки вашего приложения.
  • Она позволит избежать состояния гонки. Даже с await асинхронный код часто подвержен ошибкам. Задержка дает ощущение синхронного чтения данных, как если бы они уже были загружены.

Использование задержки на практике

До настоящего времени в Facebook мы использовали в продакшн только Relay с интегрированной задержкой. Если вы ищете практическое руководство как начать сегодня, посмотрите руководство Relay! Там рассмотрены подходы, которые уже хорошо работают у нас в продакшн.

Код примеров на этой странице использует «фальшивую» реализацию API, а не Relay. Это позволяет легче их понимать, если вы не знакомы с GraphQL, но они не расскажут вам о «правильном способе» как построить приложение с использованием задержки. Эта страница скорее о концептуальных принципах и нацелена помочь вам понять почему задержка работает определенным образом и какие проблемы она решает.

Что, если я не использую Relay?

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

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

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

Для авторов библиотек

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

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

Если у вас нет решения, которое помогает предотвращать водопады, мы предлагаем использовать API, которые поддерживают или обеспечивают получение данных до рендера. В качестве конкретного примера, посмотрите как в Relay Suspense API организована предзагрузка. Наша информация про это была не очень последовательной. Задержка для получения данных все ещё в экспериментальном режиме, наши рекомендации могут измениться со временем, так как мы учимся новому во время использования технологии в продакшн и лучше понимаем предметную область.

Классические подходы против задержки

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

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

  • «Получаем после рендера»‎ (например, fetch в useEffect): Начинаем рендерить компоненты. Каждый из этих компонентов может вызвать получение данных в своих «эффектах» и методах жизненного цикла. Этот подход обычно ведёт к «водопадам».
  • «Получаем потом рендерим»‎ (например, Relay без задержки): Начинаем получать все данные для следующего экрана как можно раньше. Когда данные готовы — рендерим новый экран. Мы ничего не можем делать пока не получим все данные.
  • «Рендерим во время получения данных»‎ (например, Relay с задержкой): Начинаем получать все требуемые данные для следующего экрана как можно раньше и начинаем рендерить новый экран немедленно — до того, как получим ответ от сети. По мере поступления данных, React повторяет рендер компонентов, которым всё ещё нужны данные, до тех пор, пока они не будут готовы.

Примечание

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

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

Подход 1: «Получаем после рендера»‎ (задержка не используется)

Распространённый сегодня способ получения данных в React с использованием эффекта:

// В функциональном компоненте:
useEffect(() => {
  fetchSomething();
}, []);

// Или в классовом компоненте:
componentDidMount() {
  fetchSomething();
}

Мы зовем этот подход «получаем после рендера» потому что получение данных начинается после рендера компонента на экране. Это приводит к проблеме, известной как «водопад».

Взгляните на компоненты <ProfilePage> и <ProfileTimeline>:

function ProfilePage() {
  const [user, setUser] = useState(null);

  useEffect(() => {    fetchUser().then(u => setUser(u));  }, []);
  if (user === null) {
    return <p>Loading profile...</p>;
  }
  return (
    <>
      <h1>{user.name}</h1>
      <ProfileTimeline />
    </>
  );
}

function ProfileTimeline() {
  const [posts, setPosts] = useState(null);

  useEffect(() => {    fetchPosts().then(p => setPosts(p));  }, []);
  if (posts === null) {
    return <h2>Loading posts...</h2>;
  }
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}

Посмотреть пример на CodeSandbox

Если вы запустите этот код в логах консоли вы увидите следующую последовательность:

  1. Начинаем получать информацию о пользователе
  2. Ждём…
  3. Закончили получать информацию о пользователе
  4. Начинаем получать сообщения пользователя
  5. Ждём…
  6. Закончили получать сообщения пользователя

Если получение информации о пользователе занимает три секунды, мы начнём получать сообщения пользователя только через три секунды! Это «водопад»: непреднамеренная последовательность, которая должна выполняться параллельно.

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

Подход 2: «Получаем потом рендерим»‎ (задержка не используется)

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

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

function fetchProfileData() {
  return Promise.all([
    fetchUser(),
    fetchPosts()
  ]).then(([user, posts]) => {
    return {user, posts};
  })
}

В этом примере <ProfilePage> ждёт окончания обоих запросов, но запускает их параллельно:

// Запускаем получение данных как можно раньшеconst promise = fetchProfileData();
function ProfilePage() {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState(null);

  useEffect(() => {    promise.then(data => {      setUser(data.user);      setPosts(data.posts);    });  }, []);
  if (user === null) {
    return <p>Loading profile...</p>;
  }
  return (
    <>
      <h1>{user.name}</h1>
      <ProfileTimeline posts={posts} />
    </>
  );
}

// Дочерний компонент теперь не запускает получение данных
function ProfileTimeline({ posts }) {
  if (posts === null) {
    return <h2>Loading posts...</h2>;
  }
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}

Посмотреть пример на CodeSandbox

Последовательность событий теперь выглядит так:

  1. Начинаем получать информацию о пользователе
  2. Начинаем получать сообщения пользователя
  3. Ждём…
  4. Закончили получать информацию о пользователе
  5. Закончили получать сообщения пользователя

Мы разрешили предыдущий сетевой «водопад», но случайно организовали другой. Мы ждем все данные используя Promise.all() внутри fetchProfileData, поэтому мы не можем рендерить профиль пользователя, пока все его сообщения не будут получены. Нам приходится ждать и то, и другое.

Конечно, это можно исправить в данном примере. Мы можем убрать вызов Promise.all() и ждать оба промиса отдельно. Однако этот подход прогрессивно усложняется с увеличением количества данных и ростом дерева компонентов. Сложно писать надежные компоненты когда произвольные части дерева данных могут исчезать или устаревать. Поэтому рендер после получения всех данных для нового экрана часто является более практичным вариантом.

Подход 3: «Рендерим во время получения данных»‎ (используем задержку)

В предыдущем подходе мы получали данные перед вызовом setState:

  1. Начинаем получать
  2. Заканчиваем получать
  3. Начинаем рендерить

С задержкой мы все ещё начинаем сначала получать данные, но мы меняем последние два пункта местами:

  1. Начинаем получать
  2. Начинаем рендерить
  3. Заканчиваем получать

С задержкой мы не ждём ответа перед тем, как начать рендерить. На самом деле, мы начинаем рендерить почти сразу после отправки сетевого запроса:

// Это не промис. Это специальный объект из нашей интеграции задержки.
const resource = fetchProfileData();
function ProfilePage() {
  return (
    <Suspense fallback={<h1>Loading profile...</h1>}>
      <ProfileDetails />
      <Suspense fallback={<h1>Loading posts...</h1>}>
        <ProfileTimeline />
      </Suspense>
    </Suspense>
  );
}

function ProfileDetails() {
  // Пробуем прочитать информацию о пользователе, хотя она всё ещё может быть не загружена
  const user = resource.user.read();  return <h1>{user.name}</h1>;
}

function ProfileTimeline() {
  // Пробуем прочитать сообщения пользователя, хотя они всё ещё могут быть не загружены
  const posts = resource.posts.read();  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}

Посмотреть пример на CodeSandbox

Вот что происходит, когда мы рендерим <ProfilePage> на экране:

  1. Мы уже отправили запросы в fetchProfileData(). Это дает нам специальный «ресурс» вместо промиса. В более реалистичном примере, он был бы предоставлен нашей задержкой интегрированной в библиотеку данных, например Relay.
  2. React пробует рендерить <ProfilePage>. Он возвращает <ProfileDetails> и <ProfileTimeline> в виде дочерних компонентов.
  3. React пробует рендерить <ProfileDetails>. Он вызывает resource.user.read(). Данные ещё не получены, поэтому компонент «задерживается». React пропускает его и пробует рендерить другие компоненты в дереве.
  4. React пробует рендерить <ProfileTimeline>. Он вызывает resource.posts.read(). Снова, данных ещё нет, этот компонент тоже «задерживается». React пропускает его тоже и пробует рендерить другие компоненты в дереве.
  5. Больше нечего рендерить. Так как <ProfileDetails> задерживается, React показывает ближайшую заглушку <Suspense> поверх него в дереве: <h1>Loading profile...</h1>. На данный момент мы закончили.

Объект resource представляет данные, которых ещё нет, но они в итоге могут загрузиться. Когда мы вызываем read(), мы или читаем данные, или компонент «задерживается».

Чем больше потоков данных поступает, React будет пробовать рендерить и каждый раз он сможет продвигаться «глубже». Когда данные resource.user получены, компонент <ProfileDetails> успешно отрендерится и нам больше не потребуется заглушка <h1>Loading profile...</h1>. В итоге мы получим все данные и заглушек больше не будет на экране.

Здесь есть интересный момент. Даже если мы используем клиент GraphQL, который собирает все требования к данным в единый запрос, потоковая передача ответа позволяет нам быстрее показать больше контента. Так как мы рендерим во время получения (в отличие от после получения) и если user появится в ответе раньше, чем posts, мы сможем «убрать» внешнюю заглушку <Suspense> до окончания ответа. Мы могли упустить это ранее, но даже при подходе «получаем потом рендерим»‎ решение содержит водопад: между получением и рендером. Задержка не страдает от этого водопада и библиотеки, такие как Relay пользуются этим.

Обратите внимание, как мы избавились от проверок if (...) «идет загрузка» в наших компонентах. Это не только убирает шаблонный код, но и упрощает быстрые изменения в дизайне. Например, если мы хотим, чтобы информация о пользователе и его сообщения всегда появлялись вместе, мы можем удалить разделение <Suspense> между ними. Или мы можем сделать их независимыми друг от друга, предоставив каждому собственный <Suspense>. Задержка позволяет нам изменить степень раздробленности наших состояний загрузки и управлять их последовательностью без больших изменений в коде.

Начинаем получать данные рано

Если вы работаете над библиотекой получения данных, есть важный аспект в подходе «рендерим во время получения данных»‎, который не стоит упускать. Мы запускаем получение данных перед рендером. Посмотрите внимательно на этот пример:

// Начинаем получать данные рано!
const resource = fetchProfileData();

// ...

function ProfileDetails() {
  // Пытаемся прочитать информацию о пользователе
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

Посмотреть пример на CodeSandbox

Обратите внимание, что вызов read() в этом примере не начинает получение данных. Он всего лишь пытается прочитать данные, которые уже были получены. Это различие очень важно для создания быстрых приложений с задержкой. Мы не хотим откладывать загрузку данных до начала рендера компонента. Как автор библиотеки получения данных, вы можете предотвратить это сделав невозможным получить объект resource до начала получения данных. Каждый пример на этой странице использует наш «фальшивый API», который обеспечивает это.

Вы можете возразить, что получение «в самом начале», как в этом примере — это не практично. Что мы собираемся делать, если перейдем на профиль другого пользователя? Мы хотели бы получать данные на основе пропсов. Ответ на этот вопрос: вместо этого нужно начать получение данных в обработчике событий. Ниже упрощенный пример навигации между страницами пользователей:

// Первое получение данных: как можно скорееconst initialResource = fetchProfileData(0);
function App() {
  const [resource, setResource] = useState(initialResource);
  return (
    <>
      <button onClick={() => {
        const nextUserId = getNextId(resource.userId);
        // Следующее получение данных: когда пользователь кликает        setResource(fetchProfileData(nextUserId));      }}>
        Next
      </button>
      <ProfilePage resource={resource} />
    </>
  );
}

Посмотреть пример на CodeSandbox

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

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

Мы все ещё в этом разбираемся

Задержка — гибкий механизм без большого количества ограничений. А код продукта должен быть более ограничен, чтобы гарантировать отсутствие водопадов и есть различные способы предоставить эти гарантии. На данный момент мы изучаем вопросы, которые включают в себя:

  • Раннее получение данных может быть громоздким для написания. Как проще избегать водопадов?
  • Когда мы получаем данные для страницы, может ли API включать в себя данные для мгновенных переходов?
  • Сколько живёт ответ сервера? Кэширование должно быть локальным или глобальным? Кто управляет кэшем?
  • Могут ли прокси помочь организовать ленивую загрузку из API без использования вызовов read()?
  • Как будет выглядеть эквивалент компоновки запросов GraphQL для произвольных данных в задержке?

Relay отвечает по своему на некоторые из этих вопросов. Есть определенно больше чем один способ сделать это и мы с нетерпением хотим увидеть, какие новые идеи появятся в сообществе разработчиков React.

Задержка и состояние гонки

Состояние гонки — это баги, которые возникают во время неправильного предположения о том, в каком порядке код может исполняться. Получение данных в хуке useEffect или в жизненных методах класса, например componentDidUpdate часто приводит к ним. Задержка поможет решить проблему, давайте посмотрим как.

Для демонстрации проблемы, мы добавим на верхний уровень компонент <App>, который рендерит наш <ProfilePage> с кнопкой, которая позволяет переключаться между разными профилями:

function getNextId(id) {
  // ...
}

function App() {
  const [id, setId] = useState(0);
  return (
    <>
      <button onClick={() => setId(getNextId(id))}>        Next      </button>      <ProfilePage id={id} />
    </>
  );
}

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

Состояние гонки и useEffect

Сначала мы попробуем версию оригинального примера «получаем в эффекте». Мы модифицируем его для передачи параметра id из пропсов <ProfilePage> в fetchUser(id) и fetchPosts(id):

function ProfilePage({ id }) {  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(id).then(u => setUser(u));  }, [id]);
  if (user === null) {
    return <p>Loading profile...</p>;
  }
  return (
    <>
      <h1>{user.name}</h1>
      <ProfileTimeline id={id} />    </>
  );
}

function ProfileTimeline({ id }) {  const [posts, setPosts] = useState(null);

  useEffect(() => {
    fetchPosts(id).then(p => setPosts(p));  }, [id]);
  if (posts === null) {
    return <h2>Loading posts...</h2>;
  }
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}

Посмотреть пример на CodeSandbox

Обратите внимание, как мы изменили зависимости эффекта с [] на [id] — потому что мы хотим перезапускать эффект, когда меняется id. В противном случае мы не будем получать новые данные.

Если мы запустим этот код, то на первый взгляд может показаться, что он работает. Однако, если мы сделаем случайным время задержки в реализации нашего «фальшивого API» и быстро нажмем кнопку «Next», мы увидим в логе консоли, что что-то пошло не так. Запросы из предыдущих профилей могут иногда «возвращаться» после того, как мы переключились на профиль с другим ID, в этом случае они могут перезаписать новое состояние устаревшим ответом от другого ID.

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

Состояние гонки и componentDidUpdate

Можно подумать, что проблема специфична для useEffect или хуков. Возможно, если мы перепишем этот код классами или используем удобный синтаксис async / await это решит проблему?

Давайте попробуем:

class ProfilePage extends React.Component {
  state = {
    user: null,
  };
  componentDidMount() {
    this.fetchData(this.props.id);
  }
  componentDidUpdate(prevProps) {
    if (prevProps.id !== this.props.id) {
      this.fetchData(this.props.id);
    }
  }
  async fetchData(id) {
    const user = await fetchUser(id);
    this.setState({ user });
  }
  render() {
    const { id } = this.props;
    const { user } = this.state;
    if (user === null) {
      return <p>Loading profile...</p>;
    }
    return (
      <>
        <h1>{user.name}</h1>
        <ProfileTimeline id={id} />
      </>
    );
  }
}

class ProfileTimeline extends React.Component {
  state = {
    posts: null,
  };
  componentDidMount() {
    this.fetchData(this.props.id);
  }
  componentDidUpdate(prevProps) {
    if (prevProps.id !== this.props.id) {
      this.fetchData(this.props.id);
    }
  }
  async fetchData(id) {
    const posts = await fetchPosts(id);
    this.setState({ posts });
  }
  render() {
    const { posts } = this.state;
    if (posts === null) {
      return <h2>Loading posts...</h2>;
    }
    return (
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.text}</li>
        ))}
      </ul>
    );
  }
}

Посмотреть пример на CodeSandbox

Этот код обманчиво легко читаем.

К сожалению, ни использование классов, ни синтаксис async / await не помог нам решить эту проблему. Эта версия подвержена состоянию гонки по тем же самым причинам.

Проблема

Компоненты React имеют собственный «жизненный цикл». Они могут получать пропсы или обновлять состояние в любой момент времени. Однако, каждый асинхронный запрос тоже имеет собственный «жизненный цикл». Он начинается когда мы его отправляем и заканчивается когда мы получаем ответ. Сложность, которую мы испытываем, заключается в «синхронизации» нескольких процессов во времени, которые влияют друг на друга. Об этом сложно думать.

Решаем состояние гонки используя задержку

Давайте перепишем этот пример с использованием задержки:

const initialResource = fetchProfileData(0);

function App() {
  const [resource, setResource] = useState(initialResource);
  return (
    <>
      <button onClick={() => {
        const nextUserId = getNextId(resource.userId);
        setResource(fetchProfileData(nextUserId));
      }}>
        Next
      </button>
      <ProfilePage resource={resource} />
    </>
  );
}

function ProfilePage({ resource }) {
  return (
    <Suspense fallback={<h1>Loading profile...</h1>}>
      <ProfileDetails resource={resource} />
      <Suspense fallback={<h1>Loading posts...</h1>}>
        <ProfileTimeline resource={resource} />
      </Suspense>
    </Suspense>
  );
}

function ProfileDetails({ resource }) {
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

function ProfileTimeline({ resource }) {
  const posts = resource.posts.read();
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}

Посмотреть пример на CodeSandbox

В предыдущем примере с задержкой у нас был только один resource, поэтому мы запишем его в переменную на верхнем уровне. Сейчас у нас несколько ресурсов, мы переместили их в состояние компонента <App>:

const initialResource = fetchProfileData(0);

function App() {
  const [resource, setResource] = useState(initialResource);

Когда мы кликаем «Next», компонент <App> делает запрос для следующего профиля и передает этот объект вниз к компоненту <ProfilePage>:

  <>
    <button onClick={() => {
      const nextUserId = getNextId(resource.userId);
      setResource(fetchProfileData(nextUserId));    }}>
      Next
    </button>
    <ProfilePage resource={resource} />  </>

Снова обратите внимание, что мы не ждём ответа, чтобы поместить в состояние. Наоборот, мы устанавливаем состояние (и начинаем рендерить) сразу после отправки запроса. Как только у нас будет больше данных, React «наполнит» содержимым компоненты внутри <Suspense>.

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

Обработка ошибок

Когда мы пишем код с промисами, мы используем catch() для обработки ошибок. Как это работает с задержкой, ведь мы не ждём промисов, чтобы начать рендер?

С задержкой, обработка ошибок при получении данных работает так же, как обработка ошибок при рендере — мы можем рендерить предохранитель в любом месте, чтобы «ловить» ошибки в компонентах ниже.

Сначала мы создадим компонент-предохранитель, чтобы использовать его в нашем проекте:

// Предохранители на данный момент должны быть классами.
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  static getDerivedStateFromError(error) {
    return {
      hasError: true,
      error
    };
  }
  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

Теперь мы можем добавить его в любую часть дерева, чтобы отлавливать ошибки:

function ProfilePage() {
  return (
    <Suspense fallback={<h1>Loading profile...</h1>}>
      <ProfileDetails />
      <ErrorBoundary fallback={<h2>Could not fetch posts.</h2>}>        <Suspense fallback={<h1>Loading posts...</h1>}>
          <ProfileTimeline />
        </Suspense>
      </ErrorBoundary>    </Suspense>
  );
}

Посмотреть пример на CodeSandbox

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

Что дальше?

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

Задержка отвечает на некоторые вопросы, но и поднимает свои собственные:

  • If some component «suspends», does the app freeze? How to avoid this?
  • What if we want to show a spinner in a different place than «above» the component in a tree?
  • If we intentionally want to show an inconsistent UI for a small period of time, can we do that?
  • Instead of showing a spinner, can we add a visual effect like «greying out» the current screen?
  • Why does our last Suspense example log a warning when clicking the «Next» button?

Чтобы ответить на эти вопросы, мы ссылаемся на следующий раздел о Паттернах Конкурентных Пользовательских Интерфейсов.