本文へスキップ
props の渡し方いろいろ

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

15 / 64

props の渡し方いろいろ

関数・見た目の種類・残り全部。実際によく使う 3 つ

前の章で、props の渡し方と型の付け方を見ました。 文字列や数値を渡す、という形です。

ですが props に渡せるのは、それだけではありません。JavaScript の値なら何でも渡せます

実際のアプリでよく出てくる渡し方を、3 つ見ていきます。

1. 関数を渡す(いちばん重要)

Part 0 でやった「関数は値である」を思い出してください。 値なら、props で渡せます。

function LikeButton({ count, onLike }: {
  count: number;
  onLike: () => void;   // 引数なし・戻り値なしの関数
}) {
  return <button onClick={onLike}>いいね {count}</button>;
}

LikeButton「押されたら何が起きるか」を知りません。 押されたことを伝えるだけです。 何をするかは、使う側が決めます。

押されたことを、上に伝える

ボタン自身は数え方を知らない

まだ押されていません

名前は on〜 にする

onLikeonSelectonCloseon + 起きたことで名前を付けます。

// ◯ 起きたことを伝えている
onLike, onSelect, onClose

// ✕ 何をするかを名前に入れてしまっている
increaseCount, openModal

後者だと、子が使い道を決めてしまっています。 同じボタンを別の場面で使い回せなくなります。

callback-prop.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

// 「押されたら何をするか」は決めない。呼ぶだけ
function LikeButton({
  count,
  onLike,
}: {
  count: number;
  // 関数も props で渡せる。「引数なし・戻り値なし」という型
  onLike: () => void;
}) {
  return (
    <Button size="sm" variant="outline" onClick={onLike}>
      いいね {count}
    </Button>
  );
}

export function CallbackProp() {
  const [count, setCount] = useState(0);
  const [log, setLog] = useState<string[]>([]);

  return (
    <div className="flex flex-col gap-3">
      {/* 何をするかは、使う側がここで決める */}
      <LikeButton
        count={count}
        onLike={() => {
          setCount(count + 1);
          setLog([`${count + 1} 回目`, ...log].slice(0, 3));
        }}
      />

      <p className="text-sm text-muted-foreground">
        {log.length === 0 ? "まだ押されていません" : log.join(" / ")}
      </p>
    </div>
  );
}

2. 見た目の種類を、決まった言葉で受け取る

同じ部品を、色違いで何度も使いたい。よくあります。

type Tone = "info" | "success" | "danger";

文字列なら何でも受け取れる(tone: string)ようにすると、tone="succes" のような打ち間違いが通ってしまいます。3 つに絞れば、打ち間違いはその場で赤線になります。

tone で見た目を切り替える

同じ部品で、色も余白も変えられる

お知らせです

保存しました

保存に失敗しました

狭い版(compact だけ書いた)

省略できるようにする

function Notice({ tone = "info", compact = false, children }) { ... }

= "info" と書いておくと、渡されなかったときの値になります。 よく使うものを既定にしておけば、多くの場面で tone を 書かずに済みます。

variant-prop.tsx

"use client";

import type { ReactNode } from "react";

import { cn } from "@/lib/utils";

// 見た目の種類を、決まった言葉に絞る
type Tone = "info" | "success" | "danger";

const styles: Record<Tone, string> = {
  info: "border-blue-300 bg-blue-50 text-blue-900",
  success: "border-green-300 bg-green-50 text-green-900",
  danger: "border-red-300 bg-red-50 text-red-900",
};

function Notice({
  tone = "info",
  compact = false,
  children,
}: {
  // ? を付けると任意。付けなければ必須
  tone?: Tone;
  compact?: boolean;
  children: ReactNode;
}) {
  return (
    <p
      className={cn(
        "rounded-md border text-sm",
        compact ? "px-2 py-1" : "p-3",
        styles[tone],
      )}
    >
      {children}
    </p>
  );
}

export function VariantProp() {
  return (
    <div className="flex flex-col gap-2">
      {/* tone を省くと info になる */}
      <Notice>お知らせです</Notice>
      <Notice tone="success">保存しました</Notice>
      <Notice tone="danger">保存に失敗しました</Notice>

      {/* 真偽値は、値を書かずに名前だけで true になる */}
      <Notice tone="info" compact>
        狭い版(compact だけ書いた)
      </Notice>
    </div>
  );
}

3. 残りをまとめて受け取る

入力欄を作ったとします。 そのうち placeholder を渡したくなり、maxLength も渡したくなり、type も…と続きます。

そのたびに props を 1 つ増やすのは、際限がありません。

function Field({ label, hint, className, ...rest }: FieldProps) {
  return (
    <label>
      <span>{label}</span>
      <input className={...} {...rest} />
    </label>
  );
}

...rest は Part 0 でやった分割代入の「残り全部」です。 受け取った props のうち、自分が使うものだけ名前を付けて取り出し、残りはまとめて input に流します

使う側が自由に足せる入力欄直した例

Field 側は placeholder も maxLength も知らない

} & ComponentProps<"input">;

&型どうしを合体させる記号です。 Part 0 でやった |(このどれか)とは逆で、両方の項目を持つという意味になります。

型のほうも、1 つずつ書く必要はありません。ComponentProps<"input"> と書けば「input タグが受け取れるもの全部」という意味になります。<> は Part 0 で出てきた型に渡す引数です。 打ち間違いはちゃんと止めてくれます。

自分で作った部品からも、型を取り出せる

ComponentProps に渡せるのは、"input" のようなタグ名だけではありません。自分で作った部品や、ライブラリの部品も渡せます。

// Button が受け取れるもの全部
type ButtonProps = ComponentProps<typeof Button>;

// それに、自分の項目を足す
type Props = ButtonProps & { loading?: boolean };

function SubmitButton({ loading, children, ...rest }: Props) {
  return (
    <Button disabled={loading} {...rest}>
      {loading ? "送信中…" : children}
    </Button>
  );
}

typeof Button「Button という部品そのものの型」という意味です。 そこから ComponentProps受け取れる props を取り出しています

こうしておくと、sizevariant onClick も、1 つも書かずにそのまま通ります。 Button 側に項目が増えても、こちらは直さなくて済みます。

spread-prop.tsx

"use client";

import { cn } from "@/lib/utils";
import type { ComponentProps, ReactNode } from "react";

type FieldProps = {
  label: string;
  // 要素そのものを props で渡すこともできる
  hint?: ReactNode;
  // 「input が受け取れるもの全部」を、そのまま受け取れるようにする
} & ComponentProps<"input">;

function Field({ label, hint, className, ...rest }: FieldProps) {
  return (
    <label className="flex flex-col gap-1 text-sm">
      <span className="font-medium">{label}</span>

      {/* 受け取った残りを、まとめて input に渡す */}
      <input
        className={cn("rounded-md border px-3 py-2", className)}
        {...rest}
      />

      {hint && <span className="text-muted-foreground">{hint}</span>}
    </label>
  );
}

export function SpreadProp() {
  return (
    <div className="flex flex-col gap-3">
      {/* placeholder も maxLength も、Field 側では書いていない */}
      <Field label="ニックネーム" placeholder="8 文字まで" maxLength={8} />

      <Field
        label="メールアドレス"
        type="email"
        placeholder="you@example.com"
        hint={
          <>
            通知に使います(<strong>公開されません</strong>)
          </>
        }
      />
    </div>
  );
}

理解できたか確かめる

確認クイズ

ボタン部品に渡す props の名前として適切なのは?

確認クイズ

tone の型を string ではなく 3 つの文字列の union にする理由は?

確認クイズ

{...rest} を使うと何が良い?

callback-prop.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

// 「押されたら何をするか」は決めない。呼ぶだけ
function LikeButton({
  count,
  onLike,
}: {
  count: number;
  // 関数も props で渡せる。「引数なし・戻り値なし」という型
  onLike: () => void;
}) {
  return (
    <Button size="sm" variant="outline" onClick={onLike}>
      いいね {count}
    </Button>
  );
}

export function CallbackProp() {
  const [count, setCount] = useState(0);
  const [log, setLog] = useState<string[]>([]);

  return (
    <div className="flex flex-col gap-3">
      {/* 何をするかは、使う側がここで決める */}
      <LikeButton
        count={count}
        onLike={() => {
          setCount(count + 1);
          setLog([`${count + 1} 回目`, ...log].slice(0, 3));
        }}
      />

      <p className="text-sm text-muted-foreground">
        {log.length === 0 ? "まだ押されていません" : log.join(" / ")}
      </p>
    </div>
  );
}

この章のまとめ

  • props には関数も要素も渡せる。 JavaScript の値なら何でも
  • 値は下へ、知らせは上へ。知らせを運ぶのが on〜 の関数
  • 見た目の種類はunion で絞ると、 打ち間違いが赤線になる
  • 既定値を書いておけば省略できる。 真偽値は名前だけで true
  • {...rest}残りをまとめて流せる

callback-prop.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

// 「押されたら何をするか」は決めない。呼ぶだけ
function LikeButton({
  count,
  onLike,
}: {
  count: number;
  // 関数も props で渡せる。「引数なし・戻り値なし」という型
  onLike: () => void;
}) {
  return (
    <Button size="sm" variant="outline" onClick={onLike}>
      いいね {count}
    </Button>
  );
}

export function CallbackProp() {
  const [count, setCount] = useState(0);
  const [log, setLog] = useState<string[]>([]);

  return (
    <div className="flex flex-col gap-3">
      {/* 何をするかは、使う側がここで決める */}
      <LikeButton
        count={count}
        onLike={() => {
          setCount(count + 1);
          setLog([`${count + 1} 回目`, ...log].slice(0, 3));
        }}
      />

      <p className="text-sm text-muted-foreground">
        {log.length === 0 ? "まだ押されていません" : log.join(" / ")}
      </p>
    </div>
  );
}