Skip to content

内置组件

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

属性

属性类型必填描述
itemsSignal<any[]>保存项数组的信号。当信号变化时,For 高效地协调列表 — 添加的项被渲染,移除的项被清理。
callback(item: any) => any为每个项调用的函数。返回要为该项渲染的 JSX。返回值会被缓存,仅在项值本身发生变化时重新计算。
fallbackany当数组为空时渲染的内容。

使用示例

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[];
}>

属性

属性类型必填描述
childrenany | any[]要渲染的子节点。如果任何子节点是(或返回)Error 实例,则错误被捕获。
fallbackany捕获错误时显示的内容。可以是静态值或接收 { 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[];
}>

属性

属性类型必填描述
rootHTMLElement子节点要渲染到的目标 DOM 元素。
childrenany | 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 根节点。

没有根节点

如果 rootnullundefined,则 Portal 不渲染任何内容:

tsx
<Portal root={null} children={<span>不会显示</span>} />
// 不渲染任何内容

Defer

在实际值准备就绪时(例如等待 Promise 解析,或信号接收到非 Promise 值),显示初始占位值。

类型签名

typescript
const Defer: PC<{
    initial?: any;
    value: Signal<any>;
}>

属性

属性类型必填描述
initialanyvalue 包含 Promise 或尚未就绪时渲染的占位内容。
valueSignal<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}
        />
    );
};

渲染顺序:

  1. 立即显示 加载中...
  2. 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 会自动清理其内部信号追踪。

根据 Apache-2.0 许可证发布。