资源 (Resources)
resource 是 PreffX 的异步数据原语。它封装了一个获取器(即一个异步函数),并对外暴露响应式状态(state、error、pending)以及一组用于控制数据获取生命周期的处理函数(refetch、mutate、cancel、invalidate)。
资源具有同构性 (isomorphic):同一份组件代码既可在服务端运行(用于 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?);状态 (State)
第一个元素 res 是一个包含三个信号 (signal) 的响应式快照:
| 字段 | 类型 | 描述 |
|---|---|---|
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 会根据获取器(fetcher)的拒绝(rejection)类型自动推断,也可以显式传入:resource<string, ApiError>(fetcher)。
处理程序(Handlers)
第二个元素是一个控制对象:
| 处理程序 | 描述 |
|---|---|
refetch() | 重新执行获取器(会先中止当前正在进行的请求) |
mutate(v) | 乐观地(optimistically)设置已解析的值,无需重新获取数据 |
cancel() | 中止当前正在进行的请求,并将 pending 状态重置。不会影响缓存 |
invalidate() | 使该资源 key 对应的缓存条目失效(仅适用于显式指定的 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 | 在指定的毫秒数(ms)后中止获取操作,并将该资源标记为失败状态 |
key | string | 唯一标识符,用于缓存及在不同使用者之间共享资源 |
keepPreviousState | boolean | 在重新获取数据或依赖项发生变化导致重新执行时,保留之前的 state 状态 |
initial
当提供了 initial 选项时,数据即被视为已就绪,因此 pending 初始值为 false,且 Suspense 不会显示回退(fallback)内容:
const [user] = resource(fetchUser, { initial: cachedUser });timeout
const [user] = resource(fetchUser, { timeout: 5000 });当发生超时(timeout)时,正在进行的请求会被终止,且 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 entryinvalidate() 会清除该条目,从而促使下一个消费者重新获取数据。位置型键(即未指定 key 的情况)不参与缓存。
keepPreviousState
const [user, { refetch }] = resource(fetchUser, { keepPreviousState: true });在重新获取(refetch)数据的过程中,之前的 state 依然可见(即“使用旧数据,同时进行重新验证”),只有 error 或 pending 状态会发生更新。
中止信号(Abort signal)
获取数据的函数(fetcher)始终会接收到一个 AbortSignal 作为其第一个参数:
resource(async (signal) => {
const res = await fetch('/api/data', { signal });
return res.json();
});在服务端和客户端,其行为保持一致:
- 客户端 — 每次执行时都会创建一个真实的
AbortController。当调用refetch()、cancel()、发生超时或组件卸载(unmount)时,该控制器会触发中止(abort)。一旦信号被中止,数据和错误都不会被应用。 - 服务端 (SSR) — 在
preload()阶段,每个资源都会创建一个真实的AbortController。如果信号被中止,数据和错误同样不会被应用——这与客户端的逻辑保持一致。
注意: 中止信号只会取消请求,并不会将其转化为错误。若要表示操作失败(例如“无权限”),请在
fetcher函数中抛出错误:
resource(async () => {
if (!hasPermission()) throw new ForbiddenError('no access');
return fetchData();
});基于依赖的重新获取
如果获取函数(fetcher)在其函数体内读取了某个信号(signal),那么当该信号发生变化时,资源(resource)便会自动重新执行:
const userId = signal(1);
const [user] = resource(async () => {
return fetchUser(userId.value); // re-runs when userId changes
});服务端渲染 (SSR)
在服务端,resource 不会执行副作用(effect),而是将其获取函数(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()执行后,解析得到的值会被嵌入到标记(markup)中,并写入预加载数据脚本。- 在客户端进行“注水”(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>
);
};