Ресурсы
resource — это примитив PreffX для работы с асинхронными данными. Он оборачивает асинхронную функцию получения данных и возвращает реактивное состояние (state, error, pending), а также набор методов (refetch, mutate, cancel, invalidate) для управления жизненным циклом ресурса.
Ресурсы являются изоморфными: один и тот же код компонента выполняется как на сервере (предварительная загрузка при SSR), так и на клиенте (гидратация + реактивность) — без необходимости разделения логики.
Базовое использование
import type { PC } from 'preffx';
export const UserCard: PC = (_props, { resource, computed }) => {
const [user] = resource(async () => {
const res = await fetch('/api/user');
return res.json();
});
const display = computed(() => user.state.value?.name ?? 'loading');
return (
<article className="user">
<h1>{display}</h1>
</article>
);
};resource(fetcher) возвращает кортеж:
const [res, handlers] = resource(fetcher, options?);Состояние
Первый элемент, res, представляет собой реактивный снимок (snapshot), содержащий три сигнала:
| Поле | Тип | Описание |
|---|---|---|
state | ReadonlySignal<T | null> | Полученные данные или null во время загрузки / после возникновения ошибки |
pending | ReadonlySignal<boolean> | true в процессе выполнения запроса |
error | ReadonlySignal<E | null> | Последняя возникшая ошибка или null |
Поля state и error намеренно разделены: ошибка не является частью состояния данных, что позволяет разграничить эти две сущности.
const [res] = resource<string, ApiError>(fetchUser);
const view = computed(() => {
if (res.pending.value) return 'loading…';
if (res.error.value) return `Error: ${res.error.value.message}`;
return res.state.value;
});Тип ошибки E выводится из типа отклонения промиса(rejection type) функции fetcher либо может быть передан явно: resource<string, ApiError>(fetcher).
Обработчики
Второй элемент кортежа представляет собой объект с обработчиками:
| Обработчик | Описание |
|---|---|
refetch() | Повторно запускает fetcher (предварительно отменяя текущий активный запрос) |
mutate(v) | Оптимистично устанавливает полученное значение без повторного выполнения запроса |
cancel() | Отменяет активный запрос и сбрасывает состояние pending. Кэш не затрагивается |
invalidate() | Аннулирует кэшированную запись для key данного ресурса (только для явных ключей) |
const [user, { refetch, mutate, cancel }] = resource(fetchUser);
return (
<div>
<button onClick={refetch}>Refresh</button>
<button onClick={() => mutate({ name: 'Optimistic' })}>Set</button>
<button onClick={cancel}>Cancel</button>
</div>
);Опции
resource(fetcher, options?) принимает следующие параметры:
| Опция | Тип | Описание |
|---|---|---|
initial | T | Начальные данные. При первом рендере флаг pending остается равным false (механизм Suspense не активируется) |
timeout | number | Прерывает выполнение запроса по истечении указанного времени (в мс) и помечает ресурс как «неудачный» |
key | string | Уникальный идентификатор для кэширования и совместного использования данных разными потребителями |
keepPreviousState | boolean | Сохраняет предыдущее состояние (state) во время повторного запроса или обновления данных из-за изменения зависимостей |
initial
Если передан параметр initial, данные уже доступны, поэтому pending изначально имеет значение false, и Suspense не отображает резервный контент (fallback):
const [user] = resource(fetchUser, { initial: cachedUser });timeout
const [user] = resource(fetchUser, { timeout: 5000 });При истечении времени ожидания (таймаута) выполняющийся запрос прерывается, а для error устанавливается значение ошибки «Resource timed out» (превышено время ожидания ресурса).
key и кэширование
Использование явных ключей (key) позволяет создать кэш, привязанный к корню (root) и доступный для разных компонентов. Если два компонента используют для ресурса один и тот же key, они совместно используют одну запись state/error и выполняют запрос к источнику данных только один раз:
const [a] = resource(fetchUser, { key: 'user' });
const [b] = resource(fetchUser, { key: 'user' }); // shares the cache entryМетод invalidate() удаляет соответствующую запись, благодаря чему следующий ресурс выполняет повторную выборку данных. Позиционные ключи (которые устанавливаются при отсутствии опции key) в кэшировании не участвуют.
keepPreviousState
const [user, { refetch }] = resource(fetchUser, { keepPreviousState: true });Во время выполнения повторного запроса (refetch) предыдущее состояние (state) остается видимым (принцип «stale-while-revalidate»), а обновляются только поля error или pending.
Сигнал прерывания (Abort signal)
Функция для выполнения запроса (fetcher) всегда получает AbortSignal в качестве первого аргумента:
resource(async (signal) => {
const res = await fetch('/api/data', { signal });
return res.json();
});Это одинаково для сервера и клиента:
- Клиент — при каждом запуске создается настоящий AbortController. Он прерывается по
refetch(),cancel(), таймауту или размонтированию. Когда сигнал прерывается, ни данные, ни ошибка не применяются. - Сервер (SSR) — настоящий AbortController создается для каждого ресурса во время preload(). Если сигнал прерывается, ни данные, ни ошибка не применяются, что соответствует семантике клиента.
Примечание: прерывание сигнала отменяет запрос; это не не превращается в ошибку. Чтобы сигнализировать об ошибке (например, «нет прав), вместо этого создайте ошибку явно:
resource(async () => {
if (!hasPermission()) throw new ForbiddenError('no access');
return fetchData();
});Повторная выборка на основе зависимостей
Если функция выборки (fetcher) обращается к сигналу внутри своего тела, то при изменении этого сигнала ресурс автоматически перезапускается:
const userId = signal(1);
const [user] = resource(async () => {
return fetchUser(userId.value); // re-runs when userId changes
});Рендеринг на стороне сервера (SSR)
На сервере resource не запускает эффект, а регистрирует свою функцию получения данных (fetcher) для preload(). Один и тот же компонент работает без каких-либо изменений:
import { createRoot } from 'preffx/server';
const server = createRoot();
const { preload, serialize } = server.renderToString(App, {});
await preload(); // resolve every registered resource
const html = serialize(); // markup + preload data script- До вызова
preload()значениеstate.valueравноnull, аpending—true. - После вызова
preload()полученное значение встраивается в разметку и записывается в скрипт с предварительно загруженными данными (preload data script). - На стороне клиента в процессе гидратации (hydration) ресурс считывает предварительно загруженное значение, поэтому повторный запрос (fetch) не выполняется.
Полный пример
import type { PC } from 'preffx';
type User = { id: number; name: string };
const UserCard: PC = (_props, { resource, computed }) => {
const [user, { refetch }] = resource<User>(
async (signal) => {
const res = await fetch('/api/user', { signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
},
{ timeout: 5000 }
);
const view = computed(() => {
if (user.pending.value) return 'Loading…';
if (user.error.value) return `Error: ${user.error.value.message}`;
return user.state.value?.name;
});
return (
<div>
<p>{view}</p>
<button onClick={refetch}>Refresh</button>
</div>
);
};