Skip to content

リソース (Resources) ​

resource は、PreffX における非同期データ処理のためのプリミティブです。これはフェッチャー(非同期関数)をラップし、リアクティブな状態(state、error、pending)に加えて、フェッチのライフサイクルを制御するためのハンドラ群(refetch、mutate、cancel、invalidate)を提供します。

リソースは**アイソモーフィック(isomorphic)**です。つまり、サーバー上(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?);

状態 (State) ​

最初の要素である res は、以下の3つのシグナルを持つリアクティブなスナップショットです。

フィールド型説明
stateReadonlySignal<T | null>取得されたデータ。読み込み中やエラー発生後は null となる
pendingReadonlySignal<boolean>データ取得処理の実行中は true となる
errorReadonlySignal<E | null>直近のエラー。エラーがない場合は null となる

state と error は意図的に分離されています。エラーはデータ状態の一部ではありません。これにより、これら2つの関心事が独立した状態に保たれます。

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 は、フェッチャー(fetcher)の拒否(rejection)型から推論されますが、明示的に指定することも可能です(例: resource<string, ApiError>(fetcher))。

ハンドラ ​

2番目の要素は、制御用オブジェクトです。

ハンドラ説明
refetch()フェッチャーを再実行します(実行中のリクエストがある場合は、それを中断してから実行します)。
mutate(v)フェッチャーを再実行することなく、解決値(resolved value)を楽観的(optimistic)に設定します。
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指定したミリ秒(ms)経過後にフェッチを中断し、リソースの状態を「失敗」とします。
keystringキャッシュや、複数のコンシューマー間での共有に使用する一意の ID。
keepPreviousStateboolean再フェッチや依存関係の再実行が行われている間、直前の state を保持します。

initial ​

initial を指定すると、データがすでに利用可能な状態となるため、pending は最初から false となり、Suspense のフォールバックは表示されません。

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

timeout ​

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

タイムアウトが発生すると、実行中のリクエストは中断され、error に「Resource timed out(リソースのタイムアウト)」エラーが設定されます。

key とキャッシュ ​

明示的なキーを指定することで、ルートごとにキャッシュを有効にし、複数のコンシューマー間で共有できるようになります。同じ key を使用する2つのコンポーネントは、単一の state / error エントリを共有し、データ取得は1回のみ行われます。

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 シグナル ​

フェッチャー(fetcher)には、常に第1引数として AbortSignal が渡されます。

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

サーバーとクライアントで、以下の挙動は共通しています。

  • クライアント — 実行ごとに実際の AbortController が生成されます。refetch()、cancel()、タイムアウト、あるいはアンマウントが発生すると、この AbortController は中断(abort)されます。シグナルが中断された場合、データもエラーも適用されません。
  • サーバー (SSR) — preload() 中、リソースごとに実際の AbortController が生成されます。シグナルが中断された場合、データもエラーも適用されません。これはクライアント側の挙動と一致しています。

注: シグナルを中断(abort)するとリクエストはキャンセルされますが、これはエラーにはなりません。失敗(例:「権限がない」など)を通知したい場合は、代わりにフェッチャー(fetcher)からエラーをスローしてください。

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 はエフェクトを実行するのではなく、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() の実行後、解決された値がマークアップに埋め込まれ、プリ​​ロードデータスクリプトに書き込まれます。
  • クライアント側でのハイドレーション時に、リソースはプリロードされた値を読み取り、フェッチ処理をスキップします。

完全な例 ​

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に基づいて公開されています。