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〜 にする
onLike、onSelect、onClose。on + 起きたことで名前を付けます。
// ◯ 起きたことを伝えている
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 つに絞れば、打ち間違いはその場で赤線になります。
同じ部品で、色も余白も変えられる
お知らせです
保存しました
保存に失敗しました
狭い版(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 を取り出しています。
こうしておくと、size も variant も 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>
);
}