如何在React组件中正确包装外部异步库?

时间:2019-03-26 18:00:56

标签: reactjs react-hooks

我正在开发一个库,希望它既可以作为React应用程序中的组件,也可以作为非React上下文中的组件来使用。出于说明目的,我们假设它是一个呈现用户头像的库(实际上是完全不同,但专有的,这是我能想到的最简单的 illustration )。

该库具有三个主要层:

  • 全局异步初始化层,它设置诸如语言环境的通用选项并加载一些通用的外部资源(例如样式表)
  • 有范围的异步初始化层,需要在全局层准备就绪后运行;它设置了一些作用域选项,并获取了更多的外部资源。该层所需的参数之一是传输功能的实现,以便该层知道如何与消费者的后端进行对话。
  • 呈现单个头像的底层组件(显然只有在前两层完成初始化后才能实际呈现)。

在非反应上下文中,库可以这样初始化:

// 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()} />;
};

这有效(某种程度上)。库的初始化顺序正确,没有任何东西直接依赖于要加载的内容(AvatarGlobalAvatarScope始终渲染其子对象,而Avatar组件除非有化身,否则根本不渲染任何内容已加载)。但是我不确定这是否是实现此目标的最佳方法。可以使用不同的钩子(例如useEffectuseRef来更干净地实现这两个提供程序吗?还是完全其他?

精明的观察者可能会注意到AvatarScope实现的问题:fetchUrls回调仅在一次(re -)loadAvatars的渲染,该渲染在全局范围完成初始化之后发生。如果回调是在组件内定义的,并且以某种方式依赖于其状态,则它将仅能访问在调用AvatarScope时已关闭的状态。这与例如传递给React元素的事件回调可以正常工作:这些回调始终与定义它们的组件的当前状态保持最新-每次重新渲染时它们都可能被重新附加。这可能是loadAvatars的一个很好的候选者:我想我可以在useRef()组件中创建一个引用,并将AvatarScope作为async (userIDs) => await ref.fetchUrls(userIDs)的{​​{1}}参数传递。但这是Proper™React Hooks做事的方式吗?

0 个答案:

没有答案