内置组件
PreffX 内置了四个可通过每个组件的第二个参数(utils)访问的组件。它们解决了常见的 UI 模式,无需额外依赖。
| 组件 | 用途 | 访问方式 |
|---|---|---|
For | 使用响应式项渲染列表 | utils.For |
Catch | 捕获并处理渲染错误 | utils.Catch |
Portal | 将子节点渲染到不同的 DOM 节点 | utils.Portal |
Defer | 在值就绪前显示占位内容 | utils.Defer |
For
响应式列表渲染器。接受一个信号(或普通数组)作为项列表,并且只重新渲染发生变化的项。支持在列表为空时显示可选的 fallback。
类型签名
typescript
const For: PC<{
items: Signal<any[]>;
callback: (item: any) => any;
fallback?: any;
}>属性
| 属性 | 类型 | 必填 | 描述 |
|---|---|---|---|
items | Signal<any[]> | 是 | 保存项数组的信号。当信号变化时,For 高效地协调列表 — 添加的项被渲染,移除的项被清理。 |
callback | (item: any) => any | 是 | 为每个项调用的函数。返回要为该项渲染的 JSX。返回值会被缓存,仅在项值本身发生变化时重新计算。 |
fallback | any | 否 | 当数组为空时渲染的内容。 |
使用示例
tsx
import type { PC } from 'preffx';
type Item = { id: number; name: string };
const App: PC = (_, { signal, For }) => {
const items = signal<Item[]>([
{ id: 1, name: 'Alpha' },
{ id: 2, name: 'Beta' },
{ id: 3, name: 'Gamma' },
]);
return (
<ul>
<For
items={items}
callback={(item: Item) => (
<li key={item.id}>{item.name}</li>
)}
fallback={<li>暂无项目</li>}
/>
</ul>
);
};响应式更新
当你修改信号中的数组时,DOM 会自动更新:
tsx
// 添加项目
items.value = [...items.value, { id: 4, name: 'Delta' }];
// 移除项目
items.value = items.value.filter((i) => i.id !== 2);
// 替换所有项目
items.value = [];
// -> 显示 fallback "暂无项目"数组中的信号项
数组中的每个项本身可以是一个信号。当某个项的信号值发生变化时,只有该项的输出会被重新计算:
tsx
const items = signal([
signal({ id: 1, name: 'Alpha' }),
signal({ id: 2, name: 'Beta' }),
]);
// 更新第二个项目 — 只有 "Beta" 重新渲染
items.value[1].value = { id: 2, name: 'BETA' };Catch
捕获子组件抛出(或返回)的错误,并渲染备选内容。灵感来自 React 错误边界。
类型签名
typescript
const Catch: PC<{
fallback?: any;
children?: any | any[];
}>属性
| 属性 | 类型 | 必填 | 描述 |
|---|---|---|---|
children | any | any[] | 否 | 要渲染的子节点。如果任何子节点是(或返回)Error 实例,则错误被捕获。 |
fallback | any | 否 | 捕获错误时显示的内容。可以是静态值或接收 { errors, ...extraProps } 的函数组件。 |
静态内容作为备选
tsx
import type { PC } from 'preffx';
const App: PC = (_, { Catch }) => {
return (
<div>
<Catch fallback={<div>出了点问题</div>}>
<MaybeBrokenComponent />
</Catch>
</div>
);
};函数组件作为备选
备选函数接收 errors(捕获的错误数组)以及传递给 <Catch> 的任何额外属性:
tsx
const App: PC = (_, { Catch }) => {
return (
<Catch
fallback={({ errors }) => (
<div class="error-fallback">
<h3>发生了 {errors.length} 个错误</h3>
</div>
)}
>
<UnstableComponent />
</Catch>
);
};向备选传递额外属性
tsx
const App: PC = (_, { Catch }) => {
return (
<Catch
fallback={({ errors, severity }) => (
<div class={severity}>
错误数: {errors.length}
</div>
)}
severity="critical"
>
<UnstableComponent />
</Catch>
);
};错误检测方式
Catch 递归扫描所有子节点(使用 flat(Infinity))。任何是 Error 实例(通过 instanceof Error)的子节点都会触发备选内容。
Portal
将子节点渲染到组件根节点之外的不同 DOM 节点中。适用于模态框、工具提示、下拉菜单和通知。
类型签名
typescript
const Portal: PC<{
root: HTMLElement;
children?: any | any[];
}>属性
| 属性 | 类型 | 必填 | 描述 |
|---|---|---|---|
root | HTMLElement | 是 | 子节点要渲染到的目标 DOM 元素。 |
children | any | any[] | 否 | 要在 Portal 根节点内渲染的内容。 |
基本用法
tsx
import type { PC } from 'preffx';
const Modal: PC = (_, { Portal }) => {
const portalRoot = document.getElementById('modal-root')!;
return (
<Portal root={portalRoot}>
<div class="modal-overlay">
<div class="modal-content">来自 Portal 的问候!</div>
</div>
</Portal>
);
};Portal 内的响应式内容
Portal 内的子节点是完全响应式的 — 信号会自动更新它们:
tsx
const Notification: PC<{ message: string }> = (
{ message },
{ signal, Portal },
) => {
const count = signal(0);
return (
<div>
<Portal root={document.getElementById('toast-root')!}>
<span id="notification-count">{count}</span>
</Portal>
<button onClick={() => (count.value += 1)}>+</button>
</div>
);
};清理
当父组件被销毁时,Portal 会自动清理其子节点并通过 root.replaceChildren() 清空 Portal 根节点。
没有根节点
如果 root 为 null 或 undefined,则 Portal 不渲染任何内容:
tsx
<Portal root={null} children={<span>不会显示</span>} />
// 不渲染任何内容Defer
在实际值准备就绪时(例如等待 Promise 解析,或信号接收到非 Promise 值),显示初始占位值。
类型签名
typescript
const Defer: PC<{
initial?: any;
value: Signal<any>;
}>属性
| 属性 | 类型 | 必填 | 描述 |
|---|---|---|---|
initial | any | 否 | 当 value 包含 Promise 或尚未就绪时渲染的占位内容。 |
value | Signal<any> | 是 | 一个信号,其值被观察。当信号的解析值不是 Promise 时,它会替换 initial 内容。 |
延迟 Promise
tsx
import type { PC } from 'preffx';
const SlowData: PC = (_, { signal, Defer }) => {
const data = signal(
new Promise<string>((resolve) =>
setTimeout(() => resolve('已加载!'), 2000)
)
);
return (
<Defer
initial={<div>加载中...</div>}
value={data}
/>
);
};渲染顺序:
- 立即显示
加载中... - 2 秒后,当 Promise 解析时,显示
已加载!
延迟普通(非 Promise)值
如果信号已经持有普通值(不是 Promise),Defer 会立即渲染它 — 跳过 initial:
tsx
<Defer
initial="等待中..."
value={signal('真实内容')}
/>
// 立即渲染 "真实内容"从 Promise 到普通值的响应式切换
当信号从 Promise 变为普通值时,Defer 从占位内容切换到真实内容:
tsx
const DynamicDefer: PC = (_, { signal, effect, Defer, onMount }) => {
const value = signal(new Promise(() => {})); // 待处理的 Promise
onMount(() => {
setTimeout(() => {
value.value = '终于解析了!';
}, 1000);
});
return <Defer initial="加载中..." value={value} />;
};通过 Defer 传递数组子节点
Defer 适用于任何内容,包括 JSX 节点数组:
tsx
const items = signal([
<span id="a">Alpha</span>,
<span id="b">Beta</span>,
]);
return (
<Defer
initial={[<div>加载中...</div>]}
value={items}
/>
);
// 立即渲染 <span>Alpha</span><span>Beta</span>清理
当父组件被销毁时,Defer 会自动清理其内部信号追踪。