Как правильно обернуть внешнюю асинхронную библиотеку в компонент React? - PullRequest
0 голосов
/ 26 марта 2019

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

Библиотека имеет три основных слоя:

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

В нереактивном контексте библиотека может быть инициализирована следующим образом:

// within main script initialisation
const avatarServiceReady = Avatars.init();


// as part of init code for a logical common context,
// e.g. a sidebar listing online users

// transport function
async function fetchAvatarUrls(userIDs) {
  return await fetch(...);
}

await avatarServiceReady;
const avatars = await Avatars.load(users.map(user => user.id), fetchAvatarUrls);

// render
users.forEach(user => {
  const item = document.createElement('li');
  item.textContent = user.name;

  if (user.id in avatars) {
    item.appendChild(avatars[user.id].createElement());
  }

  document.getElementById('online-users').appendChild(item);
});

Если я переведу это в символическое дерево компонентов React, оно будет выглядеть примерно так:

export const App = () => (
  // near the top of the component tree
  <AvatarGlobal>
    // within a logical common context,
    // e.g. a sidebar listing online users
    <AvatarScope userIDs={users.map(user => user.id)} fetchUrls={fetchAvatarUrls}>
      <ul id="online-users">
        {users.map(user => (
          <li>
            {user.name}
            <Avatar userId={user.id} />
          </li>
        ))}
      </ul>
    </AvatarScope>
  </AvatarGlobal>
);

// transport function (might be a single global implementation,
// but might also actually be specific for a given AvatarScope
async function fetchAvatarUrls(userIDs) {
  return await fetch(...);
}

Моя текущая реализация выглядит так:

import React, { createContext, useContext, useState } from 'react';
import { initGlobal, loadAvatars } from './common';

const AvatarReady = createContext(false);

const AvatarGlobal = ({ children }) => {
  const [state, setState] = useState(undefined);

  if (!state) {
    setState('loading');
    initGlobal().then(() => setState('ready'));
  }

  return (
    <AvatarReady.Provider value={state === 'ready'}>
      {children}
    </AvatarReady.Provider>
  );
};

const Avatars = createContext(undefined);

const AvatarScope = ({ userIDs, fetchUrls, children }) => {
  const ready = useContext(AvatarReady);
  const [pending, setPending] = useState(false);
  const [avatars, setAvatars] = useState(undefined);

  if (ready && !pending && !avatars) {
    setPending(true);
    loadAvatars(userIDs, fetchUrls).then(setAvatars);
  }

  return (
    <Avatars.Provider value={avatars}>
      {children}
    </Avatars.Provider>
  );
};

const Avatar = ({ userId }) => {
  const avatars = useContext(Avatars);

  return avatars
    && userId in avatars
    && <img src={avatars[userId].getUrl()} />;
};

Это работает (несколько). Библиотека инициализируется в правильном порядке, ничто не зависит напрямую от загружаемого материала (и AvatarGlobal, и AvatarScope всегда визуализируют свои дочерние элементы, а компонент Avatar просто не отображает ничего, если не загружены аватары). Но я не уверен, что это лучший способ реализовать это. Могут ли два провайдера быть реализованы более аккуратно с использованием различных хуков, таких как useEffect или useRef? Или что-то еще целиком?

Проницательный наблюдатель может заметить проблему с реализацией AvatarScope: обратный вызов fetchUrls передается только в loadAvatars функцию один раз , во время (повторного) рендеринга AvatarScope что происходит после завершения инициализации глобальной области. Если обратный вызов определен внутри компонента и каким-то образом зависит от его состояния, он будет иметь доступ только к состоянию, в котором он был закрыт в момент вызова loadAvatars. Это отличается от того, как, например, обратные вызовы событий, передаваемые элементам React, работают: они всегда соответствуют текущему состоянию компонента, в котором они определены, - они, вероятно, повторно присоединяются при каждом повторном рендеринге. Вероятно, это может быть хорошим кандидатом на useRef(): я думаю, я мог бы создать ссылку в компоненте AvatarScope и передать async (userIDs) => await ref.fetchUrls(userIDs) в качестве аргумента fetchUrls loadAvatars. Но является ли это правильным подходом к действию?

...