我正在开发一个库,希望它既可以作为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()} />;
};
这有效(某种程度上)。库的初始化顺序正确,没有任何东西直接依赖于要加载的内容(AvatarGlobal
和AvatarScope
始终渲染其子对象,而Avatar
组件除非有化身,否则根本不渲染任何内容已加载)。但是我不确定这是否是实现此目标的最佳方法。可以使用不同的钩子(例如useEffect
或useRef
来更干净地实现这两个提供程序吗?还是完全其他?
精明的观察者可能会注意到AvatarScope
实现的问题:fetchUrls
回调仅在一次(re -)loadAvatars
的渲染,该渲染在全局范围完成初始化之后发生。如果回调是在组件内定义的,并且以某种方式依赖于其状态,则它将仅能访问在调用AvatarScope
时已关闭的状态。这与例如传递给React元素的事件回调可以正常工作:这些回调始终与定义它们的组件的当前状态保持最新-每次重新渲染时它们都可能被重新附加。这可能是loadAvatars
的一个很好的候选者:我想我可以在useRef()
组件中创建一个引用,并将AvatarScope
作为async (userIDs) => await ref.fetchUrls(userIDs)
的{{1}}参数传递。但这是Proper™React Hooks做事的方式吗?