本文へスキップ
children を関数にする

Part 2 · コンポーネントと props

18 / 64

children を関数にする

包む側が持っている値を、中身に渡す

前の章で、children を使って組み合わせる形を見ました。 包む側は中身を知らなくてよく、使う側は好きなものを入れられる。

ですがこの形には、ひとつだけできないことがあります。

包む側が持っている値を、中身に渡すことができません。

できないことを、先に見る

開閉する入れ物を作りました。isOpen という状態を持っているのは Panel です。

children で中身を受け取る

中身から開閉の状態は見えない

いま……(開いているのか閉じているのか、ここからは分かりません)

ボタンの文字は「開く / 閉じる」と切り替わります。Panel の中では isOpen が使えるからです。 ですが中身の文は、押しても何も変わりません

では、中身の側にも「いま開いています」と出したくなったら?

<Panel>
  {/* ここから isOpen は見えない */}
  <p>いま {isOpen ? "開いています" : "閉じています"}</p>
</Panel>

書けません。isOpenPanel の中にある値で、 外側からは触れないからです。

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> のことを何も知らないのに、 自分の値をそこに流し込めています。

children を関数にする直した例

中身が開閉の状態を使えるようになった

いま 閉じています

この書き方を 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>
  );
}