Я занимаюсь разработкой библиотеки, которую я хочу использовать как компоненты в приложении 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
. Но является ли это правильным подходом к действию?