# @client-compose/react 中文文档

规范地址：https://npmcn.com/package/%40client-compose/react/

版本：0\.18\.1
npm 元数据更新时间：2026-10-30T12:46:02.961Z
中文译文更新时间：2026-11-10T01:42:35.647Z
软件包许可：MIT

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40client-compose/react
原文对照：https://npmcn.com/package/%40client-compose/react/?lang=original
项目主页：https://github.com/client-compose/client-compose/tree/main/clients/typescript/react#readme
源码仓库：https://github.com/client-compose/client-compose

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：287 kB，88 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- 运行依赖：2 个
- peer 依赖：react \>=18，@types/react \>=18
- 首次发布：2026-10-19T14:06:48.057Z
- 最新版本发布：2026-10-30T12:46:02.474Z
- 近 30 天下载量：3655（2026-10-10 至 2026-11-08，npm 官方下载统计）
- 已发布版本数：26（近 12 个月 26 个）

## 中文简介

React and React Industrial 捆绑客户端 编译 .

## 安装

```sh
npm install @client-compose/react
```

也可以使用：`pnpm add @client-compose/react`、`yarn add @client-compose/react`、`bun add @client-compose/react`

## README

# @ lient- compose/ react 客户端

React and React 原始绑定 [客户端编译](https://github.com/client-compose/client-compose)数字 : 由客户拥有的岛屿组成的服务器屏幕协议.

等级 : **核心项目** 议定书: **页:1** 反应 18+ 与 **反应 DOM、 反应原生、 服务器 渲染和反应服务器组件**

```sh
npm install @client-compose/react
```

`@client-compose/react` 仅使用 React 本身:没有 DOM API 和 包裹元素。 一样 软件包和同样的登记模式在网络、iOS和Android上通过React Introduction,以及 与任何其他 React 渲染器,在客户端,服务器或服务器组件上渲染:

| 渲染 | 使用 | 获取数据 |
| --- | --- | --- |
| 仅客户端( Vite, CRA, React End) | `<ClientComposeProvider>` + 键 `<ComposedScreen>` 或者说 `useScreen` | 在浏览器/ app 中 |
| 服务器渲染( Remix / React router, Next.js Pages router, Express +) `renderToString`(中文(简体) ) | `loadScreen` 在你的装填器里 `initialResponse` | 在服务器上; 客户端水合物不进行回推 |
| React 服务器组件( Next.js App Router) | `ComposedScreen` 从 `@client-compose/react/server` | 在服务器上; 岛屿在那里渲染 |

## 登记岛屿组成部分

岛屿道具类型来自 [`@client-compose/codegen`](https://www.npmjs.com/package/@client-compose/codegen) (一组) `"protocolImport": "@client-compose/react"` 在其 TypeScript 目标中,所以只生成文件 需要这个包). `registerComponent` 使该组成部分成为一个要素,因此岛屿组成部分 可以用钩.

```tsx
import { ReactIslandRegistry } from "@client-compose/react";
import { FollowButtonV1, ProfileHeaderV2 } from "./islands.generated";

export const registry = new ReactIslandRegistry()
  .registerComponent(ProfileHeaderV2, ProfileHeader) // ProfileHeader: (props: ProfileHeaderV2) => ReactNode
  .registerComponent(FollowButtonV1, FollowButton)
  .registerComponent(PostGridV1, PostGrid, { placeholder: PostGridSkeleton }); // while it streams in
```

## 提供配置

```tsx
<ClientComposeProvider
  config={{
    registry,
    baseUrl: "https://api.example.com/compose",
    client: { platform: "web", appVersion: "1.4.0" },
    headers: async () => ({ Authorization: `Bearer ${await getToken()}` }),
    onSkipped: (entry, reason) => log("island skipped", entry.key, reason),
    onRenderError: (key, error) => report(error, { island: key }),
  }}
>
  <App />
</ClientComposeProvider>
```

{\\fn黑体\\fs22\\bord1\\shad0\\3aHBE\\4aH00\\fscx67\\fscy66\\2cHFFFFFF\\3cH808080}请记住`useMemo`)如果在组件内构建它,因为一个新的配置对象反切.

## 显示屏幕

**`<ComposedScreen>`** 获取屏幕并将其岛屿排列为:

```tsx
<ComposedScreen
  screen="profile"
  params={{ userId }}
  loading={<Spinner />}
  renderError={(error, reload) => <Retry onPress={reload} />}
>
  {(islands) => <main className="profile">{islands}</main>}  {/* optional; default: no wrapper */}
</ComposedScreen>
```

**`useScreen` + 键 `<Island>`** 给予您对布局的完全控制, 例如反应原生 `FlatList` 与 重新拉动 :

```tsx
const screen = useScreen("profile", { userId });
if (screen.status === "loading") return <ActivityIndicator />;
if (screen.status === "error") return <Retry onPress={screen.reload} />;
return (
  <FlatList
    data={screen.islands}
    keyExtractor={(island) => island.key}
    renderItem={({ item }) => <Island island={item} />}
    refreshing={screen.refreshing}
    onRefresh={screen.reload}
  />
);
```

`useScreen` 返回时 `{ status, islands, response, error, refreshing, streaming, reload }`。在 第一次响应, 重新装入失败保留当前岛屿( q)`status: "ready"` 与 `error` 页:1.

## 流线

屏幕溪流(第4.6段),并照原样渲染:一旦屏幕的头部到来,其岛屿显示, 每个人仍然在路上 作为它的占位符, 并填入它的到来. `streaming` 真实存在直到 最后一次到达.

-   **占位符** 是你的:通过 `placeholder` 改为 `registerComponent`,通常是一个骨架 岛的大小,所以屏幕不会跳。 一个没有岛屿的岛屿在到达之前什么都看不到. 说在装货`aria-busy`,或 `accessibilityState={{ busy: true }}` 在“反应土著”中: 占位符是所有的东西.
-   **等待整个屏幕** 并立即显示它,像流出之前,设定 `progressive: false` 在配置中,或在一个屏幕上: `useScreen(screen, params, { progressive: false })`, (中文(简体) ), `<ComposedScreen progressive={false}>`屏幕的 `loading` 显示直到全部 在这里.
-   **重装** 将显示在岛上的东西保留到新岛到达为止 而不是闪回 对它的占位符.
-   一个从未到达的岛屿(连接已中断)是一个错误, `incomplete`,可重试 : `renderIslandError` 如果设置一个,请显示.
-   **原生反应**: 内置 `fetch` 立刻把所有的反应都交出来, 所以屏幕显示整个. 传出博览会的流出 显示他们到达时: `fetch: expoFetch` 在配置中,以 `import { fetch as expoFetch } from "expo/fetch"`。 。 。 .
-   您的服务器决定了头部是什么: 它标记需要的岛屿, 以及任何条件 需要数据, 请按住它( 参见您的作曲家的 README) .

## 服务器渲染

在装入器中获取屏幕 `loadScreen`,并将回复传递给 `<ComposedScreen>` (或 `useScreen`,作为 `initialResponse`。服务器使群岛进入 HTML,客户端 水合物不用再取出来. `loadScreen` 阅读屏幕的整条流,所以反应是 普通JSON,装货员可以按原样返回.

```tsx
// e.g. a React Router / Remix loader
export async function loader({ params, request }) {
  return { screen: await loadScreen(serverConfig(request), "profile", { userId: params.userId }) };
}

export default function Profile() {
  const { screen } = useLoaderData<typeof loader>();
  const { userId } = useParams();
  return <ComposedScreen screen="profile" params={{ userId }} initialResponse={screen} />;
}
```

`loadScreen(config, screen, params)` 使用与提供者相同的配置( n)`registry`, (中文(简体) ), `baseUrl`, (中文(简体) ), `client`, (中文(简体) ), `headers`,...),因此可以在服务器上从收到的请求中读取认证. 当你 框架重运行加载器(导航、再验证),新的 `initialResponse` 用于无 浏览器请求; 无服务器响应的参数会像往常一样从浏览器中获取 .

## 反应服务器组件

`@client-compose/react/server` 导出一个 async 服务器组件 。 屏幕被取出,它的 服务器上的岛屿; 岛屿可以是服务器组件( 没有客户端 JavaScript) 或客户端 组件( 文件标有 `"use client"`) (中文(简体) ).

```tsx
// app/profile/[userId]/page.tsx
import { ComposedScreen } from "@client-compose/react/server";

export default async function Page({ params }: { params: Promise<{ userId: string }> }) {
  const { userId } = await params;
  return (
    <ComposedScreen
      config={{
        registry,
        baseUrl: process.env.COMPOSER_URL!,
        client: { platform: "web", appVersion: "1.4.0" },
        headers: async () => ({ Authorization: `Bearer ${await tokenFromCookies()}` }),
        requestInit: { cache: "no-store" }, // personalised screens: don't share Next's fetch cache
      }}
      screen="profile"
      params={{ userId }}
      fallback={<Spinner />}        // streamed while loading; omit to use your loading.tsx
      renderError={() => <Oops />}  // omit to throw to error.tsx
    />
  );
}
```

-   从服务器组件中导入注册模块;它没有钩子,所以那里安全. 这个 软件包的主条目也可以安全地从 Server 组件导入:它的钩和组件 住在一个 `"use client"` 模块.
    
-   `requestInit` 传递到每个请求,所以框架获取选项,例如 `{ next: { revalidate: 30 } }` 工作也一样.
    
-   要刷新,请使用您框架的机制(例如:. `router.refresh()`) (中文(简体) ).
    
-   动作的更改和更新到达浏览器中的屏幕:客户端组件岛屿应用 他们自己。 浏览器 `<ClientComposeProvider>` 需要一个知道每个 岛屿,它们拥有哪些实体,哪些岛屿的行动可以更新。 群岛 服务器组件不能在那里被导入, 因此声明它们: `browserRegistry.declare(PageHeaderV1, TodoListV1)`当服务器已过时(其屏幕已失效,或 改变感动了浏览器无法改变的岛屿, `onStale` 名为:呼叫 `router.refresh()` 在这里。 没什么需要服务器的 路由器从缓存带回的一页( 返回) 跟踪自显示以来采取的行动:其客户部分岛屿适用 他们错过,和 `onStale` 如果其中之一 或岛屿是一个服务器组件, 错过了只有服务器可以显示的东西 .
    
-   屏幕溪流:岛屿头部作为占位者仍然悬而未决,每个岛屿位于 拥有 `<Suspense>`,然后在他们到达时进入页面. `progressive={false}` (或 配置的 `progressive: false`等待整个屏幕代替.
    
-   如果页面也需要屏幕(例如: `meta` 在下一个.js的 `generateMetadata`),打开其 流经一次 `streamScreen` 并传入,所以组件不再取取. 元数据 只需要头,所以岛屿仍然溪流:
    
    ```tsx
    const openArticle = cache((id: string) => streamScreen(config, "article", { articleId: id }));
    
    export async function generateMetadata({ params }) {
      const { head } = await openArticle((await params).articleId);
      return { title: head.meta?.title, description: head.meta?.description };
    }
    
    export default async function Page({ params }) {
      const { articleId } = await params;
      return <ComposedScreen config={config} screen="article" params={{ articleId }} stream={openArticle(articleId)} />;
    }
    ```
    
    或通过整个 `response` 从 `loadScreen`,使不流畅.
    
-   `serverComposer(config, { client })` 问作曲家 请求最多一次需要什么, 不管很多组件都问( React's `cache`: `openScreen(screen, params)` 页 次 元数据, `appStrings()` 传递给 `<AppStringsProvider initial>`,以及 `strings(AppStrings)` 用于服务器组件本身的单词. `client` 找出每个请求是谁要求的 比如 其语言来自 cookie 或 Accept-Language(`preferredLocale(header)`) (中文(简体) ). 做一次,在一个 模块:
    
    ```tsx
    export const composer = serverComposer(config, {
      client: async () => ({ ...config.client, locale: preferredLocale((await headers()).get("accept-language")) }),
    });
    
    export async function generateMetadata({ params }) {
      const { head } = await composer.openScreen("article", await params);
      return { title: head.meta?.title };
    }
    
    export default async function Page({ params }) {
      const p = await params;
      return <ComposedScreen config={config} screen="article" params={p} stream={composer.openScreen("article", p)} />;
    }
    ```
    
-   `onRenderError` 在此配置中看到在创建岛屿元素时抛出错误 服务器。 服务器组件岛内出错被重新扔入浏览器并捕获 那里;报告这些,放一个 `<ClientComposeProvider>` 与 `onRenderError` 上页.
    

## 字符串

应用程序显示的每个词都来自作曲家(spec §4.7),所以复制更改,以及新语言 船,没有释放。 岛上的申报 让他们成为 `t`数字 :

```tsx
function ArticleList(props: IslandProps<typeof ArticleListV1>) {
  return <h2>{props.t.heading}: {props.t.articleCount({ count: props.total })}</h2>;
}
registry.registerComponent(ArticleListV1, ArticleList);
```

`IslandProps<typeof ArticleListV1>` 是岛上的道具,有 `t` 当它宣布弦.

带有链接的句子对翻译者来说是完整的:用自己的来填充字符串的参数 占位符,以及 `richText` 将节点放入( 它没有钩子, 因此服务器组件可以使用 ) :

```tsx
richText(t.newHere({ link: "{link}" }), { link: <a href="/sign-up">{t.signUp}</a> })
```

`useLocale()` 是客户端要求的语言(`config.client.locale`对于什么应用 格式本身: `new Intl.DateTimeFormat(useLocale())`。 。 。 .

`t` 它不能从服务器组件发送到客户端组件,所以 客户组件岛 `@client-compose/react/server` 获得它 `t` 新建于 浏览器代替。 岛上没什么变化.

应用字符串(任何岛屿外的词)来自 `<AppStringsProvider>`,在提供者内部:

```tsx
<ClientComposeProvider config={config}>
  <AppStringsProvider
    cache={stringsCache} // e.g. over AsyncStorage or localStorage
    loading={<Splash />}
    failed={(retry) => <CouldNotConnect onRetry={retry} />}
  >
    <App />
  </AppStringsProvider>
</ClientComposeProvider>

function RetryButton() {
  const t = useAppStrings(AppStrings);
  return <button>{t.retry}</button>;
}
```

带他们进来的 `config.client.locale`与一个 `cache`,应用程序从它保持的字符串开始 最后一次,然后在背景中再次取出它们;没有一次,每一次发射都会等待它们. `failed` 仅当没有字符串并且无法联系到作曲家时才会显示: 一台屏幕,它的字 应用必须坚持自己。 过 `initial` 已获取字符串 (例如由服务器组件 `fetchStrings` 从 `@client-compose/react/server`),以及 `viaProxy({ loadStrings })` 通过您的服务器获取 .

## 有页的岛屿

页面岛(spec §4.5)作为道具获得第一页,以及 `next` 输入时 再来. `usePages` 把他们从屏幕上拿来 打开 网络,例如带有“显示更多”按钮:

```tsx
function Results(props: ResultListV1) {
  const { pages, status, loadMore } = usePages(props);
  return (
    <section>
      <ul>
        {pages.flatMap((page) => page.items).map((item) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
      {status === "more" && <button onClick={loadMore}>Show more</button>}
      {status === "loading" && <p role="status">Loading…</p>}
      {status === "failed" && <button onClick={loadMore}>Try again</button>}
    </section>
  );
}
```

或者在 React Industrial 中, 将下一页作为列表的结尾加载 :

```tsx
function Results(props: ResultListV1) {
  const { pages, status, loadMore } = usePages(props);
  return (
    <FlatList
      data={pages.flatMap((page) => page.items)}
      renderItem={renderResult}
      onEndReached={loadMore}
      ListFooterComponent={status === "loading" ? <Spinner /> : null}
    />
  );
}
```

`status` 实值 `more`, (中文(简体) ), `loading`, (中文(简体) ), `end` 或者说 `failed` (与 `error`: 岛上的错误 作曲家,或请求的; `loadMore` 再来一次). 当岛上有新的道具时 (屏幕重新装入,或用其他参数打开)它又从它们开始,一个页面仍在打开 它的老道具的方式被放弃.

页取自 `${baseUrl}/screens/{screen}/islands/{key}` 还有你的 `headers()`获取 他们的另一种方式,设置 `loadIsland(request)` 在配置中; 通过您的服务器, `viaProxy` 给 你一个人(以下).

在测试中,回答来自 `handlers.islands["{screen}/{key}"]` 与 `islandResponse(screen, island(key, ResultListV1, props, next))`。 。 。 .

## 行动

**`useAction`** 使用生成的描述符执行动作(第5段). `run` 绝不拒绝: 它解决了结果, 一个动作宣布的错误,或者 `{ status: "failed" }`。 。 。 .

```tsx
import { useAction } from "@client-compose/react";
import { FollowUserV1 } from "./actions.generated";

function FollowButton({ userId }: { userId: string }) {
  const follow = useAction(FollowUserV1);
  const onPress = async () => {
    const res = await follow.run({ userId });
    if (res.status === "error" && res.error.code === "blocked") showToast("You can't follow this user");
    else if (res.status === "failed") showToast("Something went wrong");
  };
  return <button disabled={follow.pending} onClick={onPress}>Follow</button>;
}
```

当行动成功时,其效果在 `run` 解决,按此顺序(第5.3段):

-   **`changes`**: 改变的每个实体的动作在显示它的每一个岛屿上都会被改变(从 生成的描述符' `entities`),在每一个屏幕上,应用程序都有:显示,缓存,每个页面a 呼唤的岛屿。 无所取取. 从持有的列表中删除一个被删除的实体; a 屏幕显示在其它任何地方重新装入。 客户端服务器组件在屏幕上渲染 部分岛屿在浏览器中改变,以及 `onStale(effects)` {\\fn黑体\\fs22\\bord1\\shad0\\3ahbe\\4ah00\\fscx67\\fscy66\\2chffffff\\3ch808080}是给其他的.
-   **`update`**:每个岛屿的新条目与显示的或 已缓存;其余屏幕会保留. 无法承受的屏幕(没有带钥匙的岛屿,或者 一个仍然流入)再装入. 在服务器组件渲染的屏幕上,例如: `changes`。 。 。 .
-   **`invalidate`**: 对匹配屏幕的缓存响应被删除 。 显示的屏幕 `useScreen` 编号 : `<ComposedScreen>` 重新装入,以及 `onInvalidate(refs)` 名为. 一个页面服务器 拨打的电话 `onStale` 当屏幕失效时.
-   **`navigate`**数字 : `onNavigate(ref)` 名为. 将屏幕映射到您的路由器中的路径 .

每个动作请求都说明应用程序可以绘制哪个岛屿(`registry.capabilities()`),所以更新内容 来到版本 它知道。 将动作附加到 `${baseUrl}/actions/{action}` 还有你的 `headers()`送他们另一个 方式,设置 `performAction(request)` 在配置中.

### 通过您的服务器

要将会话保留在 httply Only cookie 中, 请通过您自己的服务器发送屏幕和动作 补之. `createComposerProxy` (从) `@client-compose/client/proxy`)是服务器的一半:它检查 浏览器发送的内容,设置客户端上下文和信头,并返回失败作为数据. `viaProxy` 是浏览器的一半: 它将服务器的功能转换为配置 `fetch`, (中文(简体) ), `performAction` 财务报告和审定财务报表 `loadIsland`。使用 Next.js 服务器动作 :

```ts
// actions.ts
"use server";
const proxy = createComposerProxy({
  baseUrl: COMPOSER_URL,
  client: async () => ({ platform: "web", appVersion, locale: await getLocale() }),
  headers: async () => ({ Authorization: `Bearer ${await getSessionToken()}` }),
  // e.g. keep a new session in a cookie, and take its token out of the response
  onActionResponse: keepSession,
});
export async function performComposerAction(body: unknown) { return proxy.performAction(body); }
export async function loadComposerScreen(body: unknown) { return proxy.loadScreen(body); }
export async function loadComposerIsland(body: unknown) { return proxy.loadIsland(body); }
```

```tsx
// in a client component
const transport = viaProxy({
  performAction: performComposerAction,
  loadScreen: loadComposerScreen,
  loadIsland: loadComposerIsland, // leave out if no island has pages
});
const config = { registry, baseUrl: "", client, ...transport };
```

为了显示动作的错误, `declaredError(follow.last)` 给已声明错误的最后一个呼叫 结束于,以及 `isUnexpected(follow.last, ["blocked"])` 说它是否结束了其他方式: 调用失败, 或者由于宣布的错误, 应用程序不会处理( 例如在它被调用后添加的代码) 页:1 为这些显示一个通用消息 .

```tsx
const error = declaredError(follow.last);
{error?.code === "blocked" && <Notice>You can't follow this user</Notice>}
{isUnexpected(follow.last, ["blocked"]) && <Notice>Something went wrong</Notice>}
```

## 测试

`@client-compose/react/testing` 使岛屿在测试 应用的方式,针对假 作曲家们. 它没有自己的测试框架或渲染器,因此它与React Testing Library合作, 反应原生测试库等.

```tsx
import { ClientComposeProvider } from "@client-compose/react";
import { actionOk, createTestConfig, island, screen } from "@client-compose/react/testing";

const { config, gateway, navigations } = createTestConfig({
  registry,
  handlers: {
    screens: { profile: () => screen("profile", [island("header", ProfileHeaderV2, { name: "Ada" })]) },
    actions: { follow: () => actionOk(FollowUserV1, { followers: 8 }, { navigate: { screen: "feed" } }) },
  },
});
render(<ClientComposeProvider config={config}><FollowButton userId="u1" /></ClientComposeProvider>);
// ...click Follow, then:
expect(gateway.actionRequests()[0].input).toEqual({ userId: "u1" });
expect(navigations).toEqual([{ screen: "feed" }]);
```

-   `createTestConfig` 给一个提供者配置 `fetch` 是假作曲家的, 与缓冲关闭. 它记录 `navigate` 财务报告和审定财务报表 `invalidate` 效果; 通过 `onNavigate` 或者说 `onInvalidate` 运行您的 和任何其他配置(例如:. `client`)来推翻默认值.
    
-   响应构建器由生成的描述符键入: `island`, (中文(简体) ), `islandError`, (中文(简体) ), `screen`, (中文(简体) ), `screenError`, (中文(简体) ), `actionOk`, (中文(简体) ), `actionError` 财务报告和审定财务报表 `actionFailure`。 。 。 .
    
-   A 类 `screen` 到达整个。 为了测试占地者和流入岛屿的岛屿,请回答一个请求: `streamedScreen("profile", [island("header", …), pending("posts", PostGridV1)])`,然后使 `send(island("posts", …))` 财务报告和审定财务报表 `end()` (或 `cut()`,如一个中断的连接.
    
-   请求没有处理器拒绝, 测试失败 。 回答其他问题(例如上传到 (存储)与 `other`。 。 。 .
    
-   将提供者包装在您的应用程序自己的提供者中(主题、翻译、会话) a `renderIsland` 自己的帮手.
    
-   一个有呼号的岛屿从屏幕上取出页面, 所以测试一下: `pagedIsland` 给 与岛相接的屏幕`element`和假作曲家的答案(`handlers`: 屏幕上显示它的第一页,然后用光标显示它的后页.
    
    ```tsx
    const paged = pagedIsland({
      island: ResultListV1,
      first: { props: { items: [ada, grace] }, next: "c2" },
      pages: { c2: { props: { items: [linus] } } },
    });
    const { config, gateway } = createTestConfig({ registry, handlers: paged.handlers });
    render(<ClientComposeProvider config={config}>{paged.element}</ClientComposeProvider>);
    ```
    

为了测试人们会看到的话, `catalogStrings(catalog)` 读取目录文件( codegen's) `catalog`,比如说. `catalog/en.json`正如作曲家会寄来的: `english.t(ArticleListV1)` 用于 岛上 `t`, (中文(简体) ), `english.island(key, ArticleListV1, props)` 对于一个有弦的岛屿, `english.appStrings` 页:1 `<AppStringsProvider initial>`,以及 `english.stringsResponse` 伪造的 编曲 `strings` 处理者。 这是英语,有英语复数规则,除非你通过一种语言.

假作曲家本身(无反应)是 `@client-compose/client/testing`。 。 。 .

### 岛屿预览

岛屿将所显示的一切当作道具,包括文字,这样每一个都可以单独预览,在 使用以下工具: [故事书](https://storybook.js.org/)测试工具箱 他们在那里,就像在测试中一样:用一个 `createTestConfig` 配置,该 应用程序字符串来自 `catalogStrings`和任何你的应用程序的提供者。 一个故事的岛屿 执行动作说明假作曲者如何回答它们,所以失败也可以预览.

```tsx
// .storybook/composer.tsx
const english = catalogStrings(catalog);

export function withComposer(handlers: FakeGatewayHandlers = {}): Decorator {
  const { config } = createTestConfig({ registry, handlers });

  return function WithComposer(Story) {
    return (
      <ClientComposeProvider config={config}>
        <AppStringsProvider initial={english.appStrings}>
          <Story />
        </AppStringsProvider>
      </ClientComposeProvider>
    );
  };
}

// src/islands/NewTodo.stories.tsx
export const TooLong: Story = {
  args: { maxLength: 80, t: english.t(NewTodoV1) },
  decorators: [
    withComposer({
      actions: {
        "add-todo": () => actionError(AddTodoV1, { code: "title_too_long", details: { maxLength: 80 } }),
      },
    }),
  ],
};
```

添加 `withComposer()` 将您预览中的每个故事 `decorators`,一个故事自己的装饰者,与 它的操纵者, 包在它内部。 在React Industrial的"故事本"中,同样的作品,客户端设定到 app的平台 (`createTestConfig({ registry, client: { platform: "ios", … } })`) (中文(简体) ). 一个呼号岛是 从屏幕上预览, 如测试: 渲染 `pagedIsland`是因为 `element`,带有 `handlers`。 。 。 .

这个 [todo example's Next.js app](https://github.com/client-compose/client-compose/tree/main/examples/todo/next) 有一本故事书是这样设置的: `pnpm storybook` 内 `examples/todo/next`。 。 。 .

## 行为

-   **环岛相撞隔离.** 每个岛屿都会在自己的错误边界内 `<Suspense>`. 投出只隐藏那座岛屿的部件 `onRenderError`; 数据 其余的屏幕都留下 在服务器渲染( React 忽略了错误边界) 时 岛屿一无所获,在浏览器中恢复,而不是将屏幕取下.
-   **错误岛屿** (作曲家们) `status: "error"`除非设置 `renderIslandError`。 。 。 .
-   **未知岛屿** (类型、版本或状态此应用程序不知道)被跳过并报告 结束 `onSkipped`。 。 。 .
-   **夹克.** 作曲者的反应被重复使用 `cache.maxAge`每个提供者,按键 屏幕和参数。 返回屏幕会立即产生 无需请求. `reload()` 总是反弹。 设定 `cache: false` 把它关掉.
-   **参数** 可以是每次渲染的新对象;请求按下其内容键.
-   **请求被取消** 当屏幕或参数改变,或组件卸载时.

## 实例

这个 [待办示例](https://github.com/client-compose/client-compose/tree/main/examples/todo) 每类都有一个应用, 每一个都与它的作曲家合作:

-   [`web`](https://github.com/client-compose/client-compose/tree/main/examples/todo/web): Vite + 反应 DOM
-   [`next`](https://github.com/client-compose/client-compose/tree/main/examples/todo/next): Next.js App 路由器,由服务器组件提供(`serverComposer`(中文(简体) )
-   [`react-native`](https://github.com/client-compose/client-compose/tree/main/examples/todo/react-native): 博览会+反应原生

## 常见问题

### @client-compose/react 是什么？

React and React Industrial 捆绑客户端 编译 \.

### 如何安装 @client-compose/react？

在项目目录运行 npm install @client-compose/react，或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖：react（\>=18）。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖，Yarn 需要手动安装。

### @client-compose/react 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### @client-compose/react 支持 ESM 还是 CommonJS？

以 ES Module 形式发布，推荐使用 import 引入。在 CommonJS 项目中，Node\.js 20\.19\+ / 22\.12\+ 可以直接 require\(\) 不含顶层 await 的 ES Module，更早的版本需要改用动态 import\(\)。

### @client-compose/react 可以商用吗？

@client-compose/react 使用 MIT 许可。宽松许可：允许商业使用、修改和再分发，通常只需在分发时保留原版权与许可声明。以上为概括，不构成法律意见，请以许可原文为准。

### @client-compose/react 的下载量有多少？

根据 npm 官方下载统计，@client-compose/react 在 2026/10/10 – 2026/11/08 这 30 天内被下载 3,655 次，日均约 122 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入，适合横向比较热度，不能直接等同于使用人数。

### @client-compose/react 还在维护吗？

最新版本 0\.18\.1 发布于 2026/10/30（10 天前）；至今共发布 26 个版本，其中过去 12 个月发布了 26 个；首次发布于 2026/10/19。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

