🚀

Astro の HTML を React コンポーネントの props に ReactNode として渡したい

に公開

はじめに

Astro 内の React コンポーネントの props に Astro の <slot /> を渡す方法のメモです
Astro には入門したばかりなので、用語含め間違いがあればご指摘ください 🙏

https://docs.astro.build/en/basics/astro-components/#slots

環境
  • Asrto: ^5.16.5
  • React: ^19.2.0
  • @astrojs/react: ^4.4.2

1. Astro の HTML を直接 children (ReactNode) として渡す

Astro に配置した React コンポーネントのタグ内に書かれが HTML が children として渡される

MyComponent.tsx
import type { ReactNode } from "react";

type MyComponentProps = {
  title?: string;
  children: ReactNode;
};

export function MyComponent({ title, children }: MyComponentProps) {
  return (
    <div>
      {title && <h1>{title}</h1>}
      <div>{children}</div>
    </div>
  );
}
index.astro
---
import { MyComponent } from "../components/MyComponent";
---
<MyComponent title="Reactコンポーネントのタイトル">
  <p>Astro から渡す children の内容</p>
  <strong>タグ内に書かれた内容が React の children になる</strong>
</MyComponent>

📝 内部的には <astro-slot> として React に渡されているらしい

2. Astro の <slot /> を children として渡す

React コンポーネントを含む Astro コンポーネントの <slot /> を React コンポーネントの children として渡す方法

ReactWrapper.astro
---
import { MyComponent } from "./MyComponent";

type Props = { title?: string };
const { title } = Astro.props as Props;
---
<MyComponent title={title}>
  <slot />
</MyComponent>
index.astro
---
import ReactWrapper from "../components/ReactWrapper";
---
<ReactWrapper title="Reactコンポーネントのタイトル">
  <p>Astro から渡す children の内容</p>
  <strong>ここに書かれた内容が slot を経由して React の children に渡される</strong>
</ReactWrapper>

別の Astro コンポーネントを <slot> で経由しても問題なく React コンポーネントに children として Astro の HTML を渡すことができる

3. React の props に Astro の HTML を渡す

🙅 NG: Attribute に直接渡したい HTML を書くことはできない

MyComponent.tsx
import type { ReactNode } from "react";

type MyComponentProps = {
  title?: string;
  content: ReactNode;
};

export function MyComponent({ title, content }: MyComponentProps) {
  return (
    <div>
      {title && <h1>{title}</h1>}
      <div>{children}</div>
    </div>
  );
}
index.astro
---
import { MyComponent } from "../components/MyComponent";
---
<MyComponent
  title="Reactコンポーネントのタイトル"
  content={(
    <>
      <p>Astro から渡す React Component に渡す内容</p>
      <strong>属性に直接書くことはできない</strong>
    </>
  )}
/>

👇️ 下記のようなエラーが発生する

🙆 OK: Astro の Fragment を使って React コンポーネントの props に渡す

Astro の slot="" 属性でつけられた名前が React の props として渡すことができる

MyComponent.tsx
import type { ReactNode } from "react";

type MyComponentProps = {
  title?: string;
  content: ReactNode;
};

export function MyComponent({ title, content }: MyComponentProps) {
  return (
    <div>
      {title && <h1>{title}</h1>}
      <div>{content}</div>
    </div>
  );
}
index.astro
---
import { MyComponent } from "../components/MyComponent";
---
<MyComponent title="Reactコンポーネントのタイトル">
  <Fragment slot="content">
    <p>Astro から渡す React Component に渡す内容</p>
    <strong>Fragment の slot 属性の名前が React コンポーネントの props 名になる</strong>
  </Fragment>
</MyComponent>

4. <slot /> プレイスホルダーを使った fallback 付きで Astro から React に ReactNode として props を渡す

<Fragment slot="React Props Name" /> と <slot /> を使うことで fallback 付きで Astro から React に ReactNode を props として渡すことができる

MyComponent.tsx
import type { ReactNode } from "react";

type MyComponentProps = {
  title?: string;
  content: ReactNode;
};

export function MyComponent({ title, content }: MyComponentProps) {
  return (
    <div>
      {title && <h1>{title}</h1>}
      <div>{children}</div>
    </div>
  );
}

React Component を使う Astro のコンポーネント

ReactWrapper.astro
---
import { MyComponent } from "./MyComponent";

type Props = { title?: string };
const { title } = Astro.props as Props;
---
<MyComponent title={title}>
  <Fragment slot="content">
    <slot>
      {/* fallback */}
      <div>コンテンツがありません</div>
    </slot>
  </Fragment>
</MyComponent>
index.astro
---
import ReactWrapper from "../components/ReactWrapper";
---
<ReactWrapper title="Reactコンポーネントのタイトル">
  <p>Astro から渡す children の内容</p>
  <strong>ここに書かれた内容が Fragment, slot を経由して React の props として渡される</strong>
</ReactWrapper>

複数の ReactNode を Astro から受け取る

MyComponent.tsx
import type { ReactNode } from "react";

type MyComponentProps = {
  title?: string;
  footer?: ReactNode;
  children: ReactNode;
};

export function MyComponent({
  title, 
  footer,
  children,
}: MyComponentProps) {
  return (
    <section>
      {title && <h1>{title}</h1>}
      <main>{children}</main>
      {footer && (
        <footer>{footer}</footer>
      )}
    </section>
  );
}
index.astro
---
import { MyComponent } from "../components/MyComponent";
---
<MyComponent title="Reactコンポーネントのタイトル">
  <div>Children として表示される</div>
  <small>複数の node があっても問題ない</small>
  <Fragment slot="footer">
    <div>footer props に渡される内容</div>
    <div>Fragment で渡す方法も複数 node があっても問題ない</div>
  </Fragment>
</MyComponent>

🥔 Tips: children 部分を slot="" 属性なしの <Fragment /> で囲っても OK

index.astro
---
import { MyComponent } from "../components/MyComponent";
---
<MyComponent title="Reactコンポーネントのタイトル">
  <Fragment>
    <div>Children として表示される</div>
  </Fragment>
  <Fragment slot="footer">
    <div>footer props に渡される内容</div>
  </Fragment>
</MyComponent>

🍠 Tips: React コンポーネントをラップした Astro コンポーネントに <slot /> として渡す場合も同様

ReactWrapper.astro
---
import { MyComponent } from "../components/MyComponent";

type Props = { title?: string };
const { title } = Astro.props as Props;
---

<MyComponent title={title}>
  <slot>
    {/* fallback content */}
    <div>Content Fallback</div>
  </slot>
  <Fragment slot="footer">
    <slot name="footer">
      {/* fallback footer */}
      <strong>Footer Fallback</strong>
    </slot>
  </Fragment>
</MyComponent>
index.astro
---
import ReactWrapper from "../components/ReactWrapper";
---
<ReactWrapper title="Reactコンポーネントのタイトル">
  <div>Children に slot 経由で渡される</div>
  <small>複数の node があっても問題ない</small>
  <Fragment slot="footer">
    <div>footer props に slot 経由で渡される内容</div>
  </Fragment>
</ReactWrapper>

まとめ

  • 🚨 Astro は React の JSX とは異なるので、Astro テンプレートの属性から HTML を渡すことはできない (エラーになる)
  • ⚠️ <Fragment /> も React の Fragment とは異なる Astro の概念なので混同しないように理解する必要がある
  • ✅ <Fragment slot="slotName" /> の slotName で囲われた中身が React Node に変換されReact コンポーネント props.slotName として渡される (この挙動の論拠が理解しきれてない)
  • ✅ slot="" 属性のない <Fragment />, name="" 属性のない <slot /> は React コンポーネントの children: ReactNode として渡される

https://docs.astro.build/en/basics/astro-components/#named-slots

https://docs.astro.build/en/reference/astro-syntax/#fragments

https://docs.astro.build/en/concepts/islands/

Discussion