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 是一个包含三个信号 (signal) 的响应式快照:

字段类型描述
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 会根据获取器(fetcher)的拒绝(rejection)类型自动推断,也可以显式传入:resource<string, ApiError>(fetcher)。

处理程序(Handlers) ​

第二个元素是一个控制对象:

处理程序描述
refetch()重新执行获取器(会先中止当前正在进行的请求)
mutate(v)乐观地(optimistically)设置已解析的值,无需重新获取数据
cancel()中止当前正在进行的请求,并将 pending 状态重置。不会影响缓存
invalidate()使该资源 key 对应的缓存条目失效(仅适用于显式指定的 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唯一标识符,用于缓存及在不同使用者之间共享资源
keepPreviousStateboolean在重新获取数据或依赖项发生变化导致重新执行时,保留之前的 state 状态

initial ​

当提供了 initial 选项时,数据即被视为已就绪,因此 pending 初始值为 false,且 Suspense 不会显示回退(fallback)内容:

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

timeout ​

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

当发生超时(timeout)时,正在进行的请求会被终止,且 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 依然可见(即“使用旧数据,同时进行重新验证”),只有 error 或 pending 状态会发生更新。

中止信号(Abort signal) ​

获取数据的函数(fetcher)始终会接收到一个 AbortSignal 作为其第一个参数:

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

在服务端和客户端,其行为保持一致:

  • 客户端 — 每次执行时都会创建一个真实的 AbortController。当调用 refetch()、cancel()、发生超时或组件卸载(unmount)时,该控制器会触发中止(abort)。一旦信号被中止,数据和错误都不会被应用。
  • 服务端 (SSR) — 在 preload() 阶段,每个资源都会创建一个真实的 AbortController。如果信号被中止,数据和错误同样不会被应用——这与客户端的逻辑保持一致。

注意: 中止信号只会取消请求,并不会将其转化为错误。若要表示操作失败(例如“无权限”),请在 fetcher 函数中抛出错误:

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

基于依赖的重新获取 ​

如果获取函数(fetcher)在其函数体内读取了某个信号(signal),那么当该信号发生变化时,资源(resource)便会自动重新执行:

tsx
const userId = signal(1);

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

服务端渲染 (SSR) ​

在服务端,resource 不会执行副作用(effect),而是将其获取函数(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() 执行后,解析得到的值会被嵌入到标记(markup)中,并写入预加载数据脚本。
  • 在客户端进行“注水”(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-2.0 许可证发布。