Skip to content

Ресурсы ​

resource — это примитив PreffX для работы с асинхронными данными. Он оборачивает асинхронную функцию получения данных и возвращает реактивное состояние (state, error, pending), а также набор методов (refetch, mutate, cancel, invalidate) для управления жизненным циклом ресурса.

Ресурсы являются изоморфными: один и тот же код компонента выполняется как на сервере (предварительная загрузка при SSR), так и на клиенте (гидратация + реактивность) — без необходимости разделения логики.

Базовое использование ​

tsx
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) возвращает кортеж:

ts
const [res, handlers] = resource(fetcher, options?);

Состояние ​

Первый элемент, res, представляет собой реактивный снимок (snapshot), содержащий три сигнала:

ПолеТипОписание
stateReadonlySignal<T | null>Полученные данные или null во время загрузки / после возникновения ошибки
pendingReadonlySignal<boolean>true в процессе выполнения запроса
errorReadonlySignal<E | null>Последняя возникшая ошибка или null

Поля state и error намеренно разделены: ошибка не является частью состояния данных, что позволяет разграничить эти две сущности.

tsx
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 данного ресурса (только для явных ключей)
tsx
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?) принимает следующие параметры:

ОпцияТипОписание
initialTНачальные данные. При первом рендере флаг pending остается равным false (механизм Suspense не активируется)
timeoutnumberПрерывает выполнение запроса по истечении указанного времени (в мс) и помечает ресурс как «неудачный»
keystringУникальный идентификатор для кэширования и совместного использования данных разными потребителями
keepPreviousStatebooleanСохраняет предыдущее состояние (state) во время повторного запроса или обновления данных из-за изменения зависимостей

initial ​

Если передан параметр initial, данные уже доступны, поэтому pending изначально имеет значение false, и Suspense не отображает резервный контент (fallback):

tsx
const [user] = resource(fetchUser, { initial: cachedUser });

timeout ​

tsx
const [user] = resource(fetchUser, { timeout: 5000 });

При истечении времени ожидания (таймаута) выполняющийся запрос прерывается, а для error устанавливается значение ошибки «Resource timed out» (превышено время ожидания ресурса).

key и кэширование ​

Использование явных ключей (key) позволяет создать кэш, привязанный к корню (root) и доступный для разных компонентов. Если два компонента используют для ресурса один и тот же key, они совместно используют одну запись state/error и выполняют запрос к источнику данных только один раз:

tsx
const [a] = resource(fetchUser, { key: 'user' });
const [b] = resource(fetchUser, { key: 'user' }); // shares the cache entry

Метод invalidate() удаляет соответствующую запись, благодаря чему следующий ресурс выполняет повторную выборку данных. Позиционные ключи (которые устанавливаются при отсутствии опции key) в кэшировании не участвуют.

keepPreviousState ​

tsx
const [user, { refetch }] = resource(fetchUser, { keepPreviousState: true });

Во время выполнения повторного запроса (refetch) предыдущее состояние (state) остается видимым (принцип «stale-while-revalidate»), а обновляются только поля error или pending.

Сигнал прерывания (Abort signal) ​

Функция для выполнения запроса (fetcher) всегда получает AbortSignal в качестве первого аргумента:

ts
resource(async (signal) => {
    const res = await fetch('/api/data', { signal });
    return res.json();
});

Это одинаково для сервера и клиента:

  • Клиент — при каждом запуске создается настоящий AbortController. Он прерывается по refetch(), cancel(), таймауту или размонтированию. Когда сигнал прерывается, ни данные, ни ошибка не применяются.
  • Сервер (SSR) — настоящий AbortController создается для каждого ресурса во время preload(). Если сигнал прерывается, ни данные, ни ошибка не применяются, что соответствует семантике клиента.

Примечание: прерывание сигнала отменяет запрос; это не не превращается в ошибку. Чтобы сигнализировать об ошибке (например, «нет прав), вместо этого создайте ошибку явно:

ts
resource(async () => {
    if (!hasPermission()) throw new ForbiddenError('no access');
    return fetchData();
});

Повторная выборка на основе зависимостей ​

Если функция выборки (fetcher) обращается к сигналу внутри своего тела, то при изменении этого сигнала ресурс автоматически перезапускается:

tsx
const userId = signal(1);

const [user] = resource(async () => {
    return fetchUser(userId.value); // re-runs when userId changes
});

Рендеринг на стороне сервера (SSR) ​

На сервере resource не запускает эффект, а регистрирует свою функцию получения данных (fetcher) для preload(). Один и тот же компонент работает без каких-либо изменений:

tsx
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) не выполняется.

Полный пример ​

tsx
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>
    );
};

Опубликовано под лицензией Apache License 2.0