🚀
Astro の HTML を React コンポーネントの props に ReactNode として渡したい
はじめに
Astro 内の React コンポーネントの props に Astro の <slot /> を渡す方法のメモです
Astro には入門したばかりなので、用語含め間違いがあればご指摘ください 🙏
環境
- 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として渡される
Discussion