0. はじめに
この記事は、学んだ技術や知識についてを備忘録としてまとめることを目的にしている。今回はReactのPropsの基本とアンチパターンについてまとめる。
1. 開発環境
- OS: macOS Sequoia 15.5
- エディタ: VS Code
- ライブラリ: React 19.2.0
- フレームワーク: Next.js 16.0.5
- ルーター: App Router
2. Propsとは
2.1. 定義
Propsとは、親コンポーネントから子コンポーネントへ渡すデータ構造のこと。
// propsは第一引数で受け取る
// propsの名称はキャメルケースで任意に命名するのが一般的
export default function Child(props) {
return (
<div>
<p>Childコンポーネント</p>
{/* 各値にアクセスするには、props.プロパティ名と書く */}
<p>props.one: {props.one}</p>
<p>props.two: {props.two}</p>
<p>props.three: {props.three}</p>
</div>
);
}
'use client';
import Child from "@/components/Child";
export default function Home() {
return (
<main>
<Child
one="親から渡された値1"
two="親から渡された値2"
three="親から渡された値3"
/>
</main>
);
}
上記のone, two, threeの部分はプロパティ名またはキーやprop名などと呼ばれる。
プロパティ名やキーと言われるのは、Propsがオブジェクトであることに由来する。つまり上記の例では、親(page.tsx)内でChileコンポーネントに3つの値を渡しているが、これは以下のようなオブジェクトになっている。
{
one: "親から渡された値1",
two: "親から渡された値2",
three: "親から渡された値3",
{
よって、あらかじめpropsをオブジェクト表記で書くことが多い。このとき、各プロパティ名に対しての型定義を定義(propsの型定義)をする。
// propsの型を定義する
type ChildProps = {
one: string;
two: number;
three: boolean;
};
// propsは第一引数で受け取る
// propsの名称は任意
export default function Child({ one, two, three }: ChildProps) {
return (
<div>
<p>Childコンポーネント</p>
{/* 各値にアクセスするには、props.プロパティ名と書く */}
<p>one(string型): {one}</p>
<p>two(number型): {two}</p>
<p>three(boolean型): {three.toString()}</p>
</div>
);
}
'use client';
import Child from "@/components/Child";
export default function Home() {
return (
<main>
<Child
one="親から渡された値1"
two={42}
three={true}
/>
</main>
);
}
※なお、propsにおけるオブジェクト表記のことを「Propsの分割代入(Destructuring Props)」と呼ぶことが慣例としてあります。これは中の動きを考えれば間違っていないのですが、個人的には直感的ではなく気持ち悪いので、本記事では一般的ではない「オブジェクト表記」と表現しました。
2.2. Propsにおける要点
Propsはイミュータブルである
子コンポーネントは受け取ったpropsを書き換えてはいけない。
タグで挟んだものは props.children として受け取ることができる
3. アンチパターン及び改修案の実装例
3.1. 不必要なデータまで渡す
3.1.1. アンチパターン例
コンポーネントが必要としていないデータまで含んだ、巨大なオブジェクトをそのまま渡してしまう。
3.1.2. 問題点
コンポーネントが何に依存しているのかが不明になる。
3.1.3. 解消例
必要な値だけを渡すようにする。
3.2. 状態の同期
アンチパータンから学ぶReactのuseStateの3.1.1. PropsをローカルStateにコピーする を参照
3.3. 値のバケツリレー
3.3.1. アンチパターン例
最も有名な問題。深くネストされた子孫コンポーネントにデータを渡すために、使用しないPropsを延々と中間コンポーネントに渡さなければならない。
import Grandchild from "./Grandchild";
import { GreatGreatGrandchildProps } from "./GreatGreatGrandchild";
export default function Child({ value }: GreatGreatGrandchildProps) {
return (
<div>
<p>Childコンポーネント</p>
<Grandchild
value={value}
/>
</div>
);
}
import GreatGrandchild from "./GreatGrandchild";
import { GreatGreatGrandchildProps } from "./GreatGreatGrandchild";
export default function Grandchild({ value }: GreatGreatGrandchildProps) {
return (
<div>
<p>Grandchildコンポーネント</p>
<GreatGrandchild
value={value}
/>
</div>
)
}
import GreatGreatGrandchild, { GreatGreatGrandchildProps } from "./GreatGreatGrandchild";
export default function GreatGrandchild({ value }: GreatGreatGrandchildProps) {
return (
<div>
<p>GreatGrandchildコンポーネント</p>
<GreatGreatGrandchild
value={value}
/>
</div>
)
}
export type GreatGreatGrandchildProps = {
value: string;
};
export default function GreatGreatGrandchild({ value }: GreatGreatGrandchildProps) {
return (
<div>
<p>GreatGreatGrandchildコンポーネント</p>
<p>親から渡された値:{value}</p>
</div>
)
}
'use client';
import Child from "@/components/Child";
export default function Home() {
return (
<main>
<Child
value="GreatGreatGrandchildに渡す値"
/>
</main>
);
}
3.3.2. 問題点
中間コンポーネントが不要なPropsに依存してしまい、冗長なだけではなく再利用生や保守性が低下する。
3.3.3. 解消例その1
コンポーネントの合成を用いてPropsのバケツリレーを解消する。React標準のchildrenPropを用いる。
import { ReactNode } from "react";
export default function Child({ children }: { children: ReactNode }) {
return (
<div>
<p>Childコンポーネント</p>
{children}
</div>
);
}
アンチパターンではChildコンポーネントは自分自身で Grandchildをインポートし、JSXの中にハードコードしていた。しかしchildrenを使うと、「中に何が入るか」の決定権を親コンポーネントに委ねることができる。
そもそも中間コンポーネントにおいて「配下に何が来るのか」、「どんなデータが必要なのか」を知る必要がなくなり、再利用しやすい汎用的なコンポーネントとなっている。
以下コンポーネントのchildrenも同様。
import { ReactNode } from "react";
export default function Grandchild({ children }: { children: ReactNode }) {
return (
<div>
<p>Grandchildコンポーネント</p>
{children}
</div>
);
}
import { ReactNode } from "react";
export default function GreatGrandchild({ children }: { children: ReactNode }) {
return (
<div>
<p>GreatGrandchildコンポーネント</p>
{children}
</div>
)
}
// 最後の子孫コンポーネントのみ変更無し
export type GreatGreatGrandchildProps = {
value: string;
};
export default function GreatGreatGrandchild({ value }: GreatGreatGrandchildProps) {
return (
<div>
<p>GreatGreatGrandchildコンポーネント</p>
<p>親から渡された値:{value}</p>
</div>
)
}
'use client';
import Child from "@/components/Child";
import Grandchild from "@/components/Grandchild";
import GreatGrandchild from "@/components/GreatGrandchild";
import GreatGreatGrandchild from "@/components/GreatGreatGrandchild";
export default function Home() {
return (
<main>
<Child>
<Grandchild>
<GreatGrandchild>
<GreatGreatGrandchild value="GreatGreatGrandchildに渡す値" />
</GreatGrandchild>
</Grandchild>
</Child>
</main>
);
}
これにより依存関係が可視化され、page.tsxを見るだけでDOMの階層構造と、どのコンポーネントがどのデータを必要としているかがひと目で分かる。
またContext APIを使う場合、Contextの値が更新されるとConsumer配下が全て再レンダリングされるリスクがあるが、この合成アプローチであれば不必要な再レンダリングを自然に防ぐことが可能。
3.3.4. 解消例その2
Context APIを用いる※後日追記

