Part 2 · コンポーネントと props
18 / 64 章
children を関数にする
包む側が持っている値を、中身に渡す
前の章で、children を使って組み合わせる形を見ました。 包む側は中身を知らなくてよく、使う側は好きなものを入れられる。
ですがこの形には、ひとつだけできないことがあります。
包む側が持っている値を、中身に渡すことができません。
できないことを、先に見る
開閉する入れ物を作りました。isOpen という状態を持っているのは Panel です。
中身から開閉の状態は見えない
いま……(開いているのか閉じているのか、ここからは分かりません)
ボタンの文字は「開く / 閉じる」と切り替わります。Panel の中では isOpen が使えるからです。 ですが中身の文は、押しても何も変わりません。
では、中身の側にも「いま開いています」と出したくなったら?
<Panel>
{/* ここから isOpen は見えない */}
<p>いま {isOpen ? "開いています" : "閉じています"}</p>
</Panel>書けません。isOpen は Panel の中にある値で、 外側からは触れないからです。
plain-children.tsx
"use client";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";
// 開閉の状態を持つ入れ物。中身は使う側から渡してもらう
function Panel({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="rounded-lg border p-4">
<Button size="sm" onClick={() => setIsOpen(!isOpen)}>
{/* isOpen を知っているのはここだけ。children には渡せない */}
{isOpen ? "閉じる" : "開く"}
</Button>
{/* 中身はいつも出す。違いは「値を渡せるかどうか」だけ */}
<div className="mt-3">{children}</div>
</div>
);
}
export function PlainChildren() {
return (
<Panel>
{/* ここから isOpen は見えない。「開いています」と書くことができない */}
<p className="text-sm text-muted-foreground">
いま……(開いているのか閉じているのか、ここからは分かりません)
</p>
</Panel>
);
}作るのを、あとまわしにする
問題は「先に作ってしまう」ことでした。 なら、作るのを遅らせればいい。
Part 0 でやった「関数は値として渡せる」を思い出してください。map に渡していたのは、まさに「あとで呼んでもらう作り方」でした。
// map は「作り方」を受け取って、要素の数だけ呼ぶ
items.map((item) => <li>{item}</li>);同じことを children でやります。要素そのものではなく、要素の作り方を渡すのです。
faac.tsx
"use client";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";
type PanelProps = {
// children は「要素」でも「要素を返す関数」でもよい
children: ReactNode | ((state: { isOpen: boolean }) => ReactNode);
};
function Panel({ children }: PanelProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="rounded-lg border p-4">
<Button size="sm" onClick={() => setIsOpen(!isOpen)}>
{isOpen ? "閉じる" : "開く"}
</Button>
<div className="mt-3">
{/* 関数なら呼ぶ。そうでなければそのまま置く */}
{typeof children === "function" ? children({ isOpen }) : children}
</div>
</div>
);
}
export function Faac() {
return (
<Panel>
{/* 関数で受け取ると、親の状態が引数で降りてくる */}
{({ isOpen }) => (
<p className="text-sm text-muted-foreground">
いま {isOpen ? "開いています" : "閉じています"}
</p>
)}
</Panel>
);
}children を関数にする
受け取る側は、渡されたのが関数なら呼びます。 そのとき、自分が持っている値を引数で渡します。
{typeof children === "function" ? children({ isOpen }) : children}typeof は「これは何の種類の値か」を調べる書き方です。 関数なら "function" が返ります。
仕掛けはこれだけです。React の特別な機能ではありません。 「関数だったら呼ぶ」という、ただの JavaScript です。
三項演算子で分けているので、今までどおり普通の要素を渡してもそのまま動きます。 できることが増えただけで、失われたものはありません。
faac.tsx
"use client";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";
type PanelProps = {
// children は「要素」でも「要素を返す関数」でもよい
children: ReactNode | ((state: { isOpen: boolean }) => ReactNode);
};
function Panel({ children }: PanelProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="rounded-lg border p-4">
<Button size="sm" onClick={() => setIsOpen(!isOpen)}>
{isOpen ? "閉じる" : "開く"}
</Button>
<div className="mt-3">
{/* 関数なら呼ぶ。そうでなければそのまま置く */}
{typeof children === "function" ? children({ isOpen }) : children}
</div>
</div>
);
}
export function Faac() {
return (
<Panel>
{/* 関数で受け取ると、親の状態が引数で降りてくる */}
{({ isOpen }) => (
<p className="text-sm text-muted-foreground">
いま {isOpen ? "開いています" : "閉じています"}
</p>
)}
</Panel>
);
}使う側から見た形
<Panel>
{({ isOpen }) => (
<p>いま {isOpen ? "開いています" : "閉じています"}</p>
)}
</Panel>親の状態が、引数として降りてきました。Panel は <p> のことを何も知らないのに、 自分の値をそこに流し込めています。
中身が開閉の状態を使えるようになった
いま 閉じています
この書き方を FaaC(Function as a Child) または render props と呼びます。 名前は仰々しいですが、やっていることは「children に関数を置いただけ」です。
faac.tsx
"use client";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";
type PanelProps = {
// children は「要素」でも「要素を返す関数」でもよい
children: ReactNode | ((state: { isOpen: boolean }) => ReactNode);
};
function Panel({ children }: PanelProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="rounded-lg border p-4">
<Button size="sm" onClick={() => setIsOpen(!isOpen)}>
{isOpen ? "閉じる" : "開く"}
</Button>
<div className="mt-3">
{/* 関数なら呼ぶ。そうでなければそのまま置く */}
{typeof children === "function" ? children({ isOpen }) : children}
</div>
</div>
);
}
export function Faac() {
return (
<Panel>
{/* 関数で受け取ると、親の状態が引数で降りてくる */}
{({ isOpen }) => (
<p className="text-sm text-muted-foreground">
いま {isOpen ? "開いています" : "閉じています"}
</p>
)}
</Panel>
);
}いつ使うか
毎回これにする必要はありません。中身が親の状態を必要とするときだけです。
- 開閉、選択中の項目、読み込み中かどうかを中身にも使わせたい
- 一覧の並べ方は共通にしつつ、 1 件の見た目は使う側に任せたい
- マウス位置や画面幅など、測るのは共通・使い道は自由なもの
faac.tsx
"use client";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";
type PanelProps = {
// children は「要素」でも「要素を返す関数」でもよい
children: ReactNode | ((state: { isOpen: boolean }) => ReactNode);
};
function Panel({ children }: PanelProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="rounded-lg border p-4">
<Button size="sm" onClick={() => setIsOpen(!isOpen)}>
{isOpen ? "閉じる" : "開く"}
</Button>
<div className="mt-3">
{/* 関数なら呼ぶ。そうでなければそのまま置く */}
{typeof children === "function" ? children({ isOpen }) : children}
</div>
</div>
);
}
export function Faac() {
return (
<Panel>
{/* 関数で受け取ると、親の状態が引数で降りてくる */}
{({ isOpen }) => (
<p className="text-sm text-muted-foreground">
いま {isOpen ? "開いています" : "閉じています"}
</p>
)}
</Panel>
);
}理解できたか確かめる
確認クイズ
普通の children では、親の状態を中身から使えないのはなぜ?
確認クイズ
FaaC の仕組みは?
確認クイズ
FaaC にすると、今までの使い方はできなくなる?
faac.tsx
"use client";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";
type PanelProps = {
// children は「要素」でも「要素を返す関数」でもよい
children: ReactNode | ((state: { isOpen: boolean }) => ReactNode);
};
function Panel({ children }: PanelProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="rounded-lg border p-4">
<Button size="sm" onClick={() => setIsOpen(!isOpen)}>
{isOpen ? "閉じる" : "開く"}
</Button>
<div className="mt-3">
{/* 関数なら呼ぶ。そうでなければそのまま置く */}
{typeof children === "function" ? children({ isOpen }) : children}
</div>
</div>
);
}
export function Faac() {
return (
<Panel>
{/* 関数で受け取ると、親の状態が引数で降りてくる */}
{({ isOpen }) => (
<p className="text-sm text-muted-foreground">
いま {isOpen ? "開いています" : "閉じています"}
</p>
)}
</Panel>
);
}この章のまとめ
- 普通の
childrenは外側で作り終わっているので、親の値を使えない - 要素ではなく、要素の作り方(関数)を渡すと解決する
- 仕組みは「関数なら呼ぶ」だけ。React の機能ではない
- 動き(包む側)と見た目(使う側)を分けて持てる
- 入れ子が深くなったら、カスタムフックに切り出せないか考える
faac.tsx
"use client";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";
type PanelProps = {
// children は「要素」でも「要素を返す関数」でもよい
children: ReactNode | ((state: { isOpen: boolean }) => ReactNode);
};
function Panel({ children }: PanelProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="rounded-lg border p-4">
<Button size="sm" onClick={() => setIsOpen(!isOpen)}>
{isOpen ? "閉じる" : "開く"}
</Button>
<div className="mt-3">
{/* 関数なら呼ぶ。そうでなければそのまま置く */}
{typeof children === "function" ? children({ isOpen }) : children}
</div>
</div>
);
}
export function Faac() {
return (
<Panel>
{/* 関数で受け取ると、親の状態が引数で降りてくる */}
{({ isOpen }) => (
<p className="text-sm text-muted-foreground">
いま {isOpen ? "開いています" : "閉じています"}
</p>
)}
</Panel>
);
}