Неустановленные и рендеринговые ошибки при извлечении данных

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

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

Использование Unstated для получения данных

Большинство разработчиков React используют Redux (или аналогичный «поставщик хранилища») для обработки состояния в приложении React, но другие решения, такие как Apollo, пытаются бросить вызов этому способу написания приложений (и управления магазином), разрешая каждому компоненту описание требований к данным внутри функции render. Фактически, можно реализовать нечто подобное даже без Apollo / GraphQL, а Unstated - это небольшая библиотека, построенная на основе React Context, которая упрощает создание приложений, использующих такой шаблон.

Базовая версия сборщика-контейнера может быть:

import { Container } from 'unstated';
class FetchContainer extends Container {
  state = {
    data: null,
    error: null,
    loading: false,
  };
  fetch = async (url) => {
    await this.setState({ loading: true });
    try {
      const data = await fetch(url).then(r => r.json());
      await this.setState({ data, loading: false });
    } catch(error) {
      await this.setState({ error, loading: false });
    }
  }
}

И использование его в компоненте «запрос» для получения проектов может выглядеть так:

import { Subscribe } from 'unstated';
import FetchContainer from './state';
const ProjectsQuery = ({ children }) => (
  <Subscribe to={[FetchContainer]}>
    {container => {
      const { data, error, loading } = container.state;
      if (!data && !error && !loading) {
        container.fetch('/projects');
      }
      return children({ data, error, loading });
    }
  </Subscribe>
);

Теперь, предполагая, что у нас есть Unstated <Provider />, обертывающий все наше приложение, мы можем разместить столько <ProjectsQuery /> компонентов, сколько захотим, в любой части приложения, и все они будут иметь одно и то же состояние. В том, что все? Не совсем.

Асинхронный цикл рендеринга setState и React

Это может быть неочевидно, но, учитывая, что Unstated setState является асинхронным, возникает проблема: если у нас есть несколько <ProjectsQuery /> компонентов, которые монтируются одновременно, каждый из них будет запускать запрос на выборку, даже если мы ожидаем setState вызова.

Почему? Поскольку асинхронный setState означает, что пока React отрисовывает дерево, вызов, который устанавливает loading: true, все еще «в полете» на протяжении всего цикла отрисовки. Таким образом, каждый <ProjectsQuery /> компонент будет по-прежнему получать loading: false от состояния.

Вы можете избежать этого несколькими способами: добавить свойство экземпляра с именем isLoading (и вернуть его вместо state.loading) или использовать обратный вызов setState для проверки фактического состояния loading и либо запустить выборку внутри него, либо выбросить, чтобы указать, что контейнер должен пропустить снова вызываю выборку. Довольно запутанно и пахнет ... что-то не так.

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

Рендеринг реквизита ада

Несколько лет назад сообщество Node страдало от проблемы, называемой адом обратного вызова: в основном каждый раз, когда вам нужно было обрабатывать что-то асинхронно, вам приходилось передавать функцию обратного вызова, генерирующую трудноуправляемые «деревья». Позже промисы и async / await улучшили ситуацию, позволив сгладить это дерево обратных вызовов. Забавно то, что если мы в конечном итоге будем использовать рендеринг-реквизиты для всего, наши компоненты будут выглядеть так:

<CurrentUserQuery>
  {(currentUserQuery) => (
    <SettingsQuery user={currentUserQuery.data}>          
      {(settingsQuery) => (
        <ThemeQuery user={currentUserQuery.data}> 
          {(themeQuery) => (
            <ProjectsQuery user={currentUserQuery.data}>
              {(projectsQuery) => (
                projectsQuery.data &&
                projectsQuery.data.map(p => (
                  <Project {...p} settings={settingsQuery.data} theme={themeQuery.data} />
                  )
              )}
            </ProjectsQuery>
          )}
        </UserThemeQuery>
      )}
    </UserSettingsQuery>
  )}
</CurrentUserQuery>
// …and I’m not even handling errors or loading states

Вышеупомянутую ситуацию можно рассматривать как крайнюю, и есть возможности для улучшений (уже существуют десятки библиотек / утилит для объединения компонентов рендеринга в один компонент рендеринга), но если вы разрабатываете нетривиальное приложение, размер вашего дерева может почти вдвое и довольно скоро достигнет дискретного уровня render props hell, особенно если вы примените тот же шаблон для мутаций (запросы публикации / размещения).

Включит ли ожидание React другие шаблоны?

Я думаю, что Javascript как язык прошел долгий путь, чтобы предоставить инструменты для написания лучшего и простого для понимания кода. Шаблон рендеринга реквизита улучшает обнаруживаемость источника реквизита (по сравнению с компонентами высокого порядка), но за счет того, что дерево становится более глубоким и трудным для отслеживания.

Рендеринг реквизита для выборки данных может быть лишь промежуточным шагом к лучшему решению, которое, мы надеемся, будет предоставлено новым потоком асинхронного рендеринга React: Suspense. Похоже, что с ожиданием React мы сможем использовать простые «сервисы» Javascript, чтобы сигнализировать всякий раз, когда были получены наши данные, и запускать повторный рендеринг по завершении. Например:

// this is probably not the final API
const CurrentUserResource = createResource(fetchCurrentUser);
const UserSettingsResource = createResource(fetchUserSettings);
const UserThemeResource = createResource(fetchUserTheme);
const ProjectsResource = createResource(fetchProjects);
const MyProjects = () => {
  const currentUser = CurrentUserResource.read();
  const userSettings = UserSettingsResource.read(currentUser.id);
  const userTheme = UserSettingsResource.read(currentUser.id);
  return (
    ProjectsResource.read(currentUser.id).map(p => (
      <Project {...p} settings={userSettings} theme={userTheme} />
    ))
  )
}
const App = () => (
  <Placeholder delayMs={1000} fallback={<Spinner />}>
    <MyProjects />
  </Placeholder>
);

Здесь компонент Placeholder обрабатывает состояние загрузки (и, возможно, ошибки), поэтому мы можем написать наши компоненты для получения ресурсов, поскольку они уже извлечены. Действительно, приведенный выше код отражает пример использования simple-cache-provider (все еще в разработке), и каждый read() либо вернет ресурс, либо выдаст обещание, которое вызовет повторный рендеринг из Placeholder после разрешения.

В этом примере мы читаем один ресурс за раз, последовательно, но достаточно просто обновить код, чтобы распараллелить чтение некоторых ресурсов, объединив брошенные обещания в Promise.all.

Наши приложения основаны на решениях

Каждый раз, когда мы пишем строку кода, мы выбираем ее содержимое из пула сотен различных возможностей. И мы стараемся принимать оптимальные решения с учетом ограничений, с которыми мы работаем. Это означает, что существует множество допустимых вариантов использования для Unstated, рендеринга props, компонентов высокого порядка ... и улучшенная обработка загрузки ресурсов в самом React не решит всех проблем. эти шаблоны исчезают. Однако, если вы думаете о том, чтобы переписать все свое приложение, чтобы использовать реквизиты рендеринга, поскольку они являются новой, горячей штукой, то я бы посоветовал вам подождать еще немного, пока не появится ожидание React, и посмотреть, как response-redux, react-apollo и другие сообщества решат использовать эту новую функцию. Надеюсь, скоро будет.