React でコンポーネントを作っていると、「この props は指定されなかったときに primary 扱いにしたい」「渡されなければ空配列として扱いたい」といった場面が必ず出てきます。関数コンポーネントでこれを実現する現在の標準は、引数の分割代入でデフォルト値を書く方法です。この記事では基本の書き方から始めて、デフォルト値が適用されるのは props が undefined のときだけという重要なルール、TypeScript での型の付け方、React 19 で関数コンポーネントから削除された defaultProps の現在、そしてオブジェクトや配列をデフォルト値にしたときに起きる参照の問題までをまとめて解説します。
目次
基本は引数の分割代入で書く
関数コンポーネントは、props をひとつのオブジェクトとして受け取るただの関数です。そのため、JavaScript のデフォルト引数(ES6 のデフォルトパラメータ)がそのまま使えます。分割代入で props を受け取り、プロパティ名の後ろに = と初期値を書くだけです。
// variant と size にデフォルト値を設定する
function Button({ variant = 'primary', size = 'medium', children }) {
return (
<button type="button" className={`btn btn--${variant} btn--${size}`}>
{children}
</button>
);
}
export default Button;
これで、呼び出し側は必要な props だけを書けばよくなります。<Button>保存</Button> と書けば variant は 'primary'、size は 'medium' になり、<Button variant="danger">削除</Button> と書けば variant だけが 'danger' に置き換わります。
function App() {
return (
<>
{/* variant: 'primary' / size: 'medium' が使われる */}
<Button>保存</Button>
{/* variant だけ上書き。size は 'medium' のまま */}
<Button variant="danger">削除</Button>
{/* 両方とも上書き */}
<Button variant="secondary" size="small">キャンセル</Button>
</>
);
}
分割代入を使わず props オブジェクトのまま受け取っている場合でも、後から個別に取り出すときに同じ書き方ができます。ただしコンポーネントの引数の位置で書いておくほうが、「このコンポーネントはどんな props を受け取り、省略したときどうなるか」が一目で分かるため読みやすくなります。
function Button(props) {
// 関数の中で分割代入してもデフォルト値は同じように書ける
const { variant = 'primary', size = 'medium', children } = props;
return <button className={`btn btn--${variant} btn--${size}`}>{children}</button>;
}
デフォルト値が使われるのは undefined のときだけ
ここが分割代入のデフォルト値でもっとも誤解されやすいところです。デフォルト値が使われる条件は「props が省略されたとき」ではなく、受け取った値が undefined のときです。JavaScript のデフォルト引数の仕様がそのまま適用されるため、null や 0、空文字を渡した場合はデフォルト値に置き換わりません。
function Badge({ count = 1 }) {
return <span className="badge">{count}</span>;
}
この Badge に対して、いろいろな渡し方をしたときの count の値は次のようになります。
| 呼び出し方 | コンポーネント内の count | デフォルト値 |
|---|---|---|
<Badge /> | 1 | 使われる |
<Badge count={undefined} /> | 1 | 使われる |
<Badge count={null} /> | null | 使われない |
<Badge count={0} /> | 0 | 使われない |
<Badge count={false} /> | false | 使われない |
注目したいのは2行目です。count={undefined} と明示的に undefined を渡した場合も、props を省略したときと同じ扱いになりデフォルト値が使われます。逆に null は「値が渡された」と判断されるため、そのまま null が入ります。0 や false のような「falsy な値」も、れっきとした値なのでデフォルトには置き換わりません。
null を渡してしまう典型パターン
この挙動が問題になるのは、API から取得したデータをそのまま props に流し込むときです。多くの API は「値なし」を null で表現します。JSON には undefined という値が存在しないため、JSON.parse() を通った時点で欠損値はほぼ確実に null になります。
function UserCard({ nickname = '名無しさん' }) {
// nickname が null だと length の参照でエラーになる
return <p>{nickname}({nickname.length}文字)</p>;
}
function Page({ user }) {
// API のレスポンスが { "nickname": null } だった場合、
// デフォルト値は使われず null がそのまま渡る
return <UserCard nickname={user.nickname} />;
}
「デフォルト値を書いたのに Cannot read properties of null が出る」という状況は、ほとんどがこのパターンです。null も含めて初期値にフォールバックさせたいなら、分割代入のデフォルト値ではなく null 合体演算子(??)を使います。
?? や || との使い分け
初期値へのフォールバックには3つの書き方があり、それぞれ「どの値のときに初期値が使われるか」が違います。
| 書き方 | 初期値が使われる条件 | 向いている用途 |
|---|---|---|
{ variant = 'primary' } | undefined のときだけ | props を省略できるようにする通常のケース |
value ?? 'primary' | undefined と null のとき | API のレスポンスなど null が来る値 |
value || 'primary' | 上記に加えて 0・''・false のときも | 空文字も「未入力」として扱いたいとき |
実務では、分割代入のデフォルト値と ?? を組み合わせるのが安全です。分割代入で受け取ったあと、null の可能性がある値だけを ?? で処理します。
function UserCard({ nickname }) {
// undefined でも null でも '名無しさん' になる
const displayName = nickname ?? '名無しさん';
return <p>{displayName}({displayName.length}文字)</p>;
}
|| を使うのは避けたほうが無難です。数値の 0 や空文字が正当な値であるケースは多く、count || 10 と書くと「0件」を表示したいときに 10 が表示されてしまいます。空文字を未入力とみなしたい、という明確な意図があるときだけに限定してください。
TypeScript ではオプショナルプロパティと組み合わせる
TypeScript で書く場合は、デフォルト値を持つ props を型定義でオプショナルプロパティ(?)にするのが基本です。? を付けることで呼び出し側は省略でき、省略されたときに分割代入のデフォルト値が働きます。
import type { ReactNode } from 'react';
type ButtonProps = {
children: ReactNode; // 必須
variant?: 'primary' | 'secondary' | 'danger'; // 省略可
size?: 'small' | 'medium' | 'large'; // 省略可
};
function Button({ children, variant = 'primary', size = 'medium' }: ButtonProps) {
// ここでの variant の型は 'primary' | 'secondary' | 'danger'
// デフォルト値があるため undefined は含まれない
return (
<button type="button" className={`btn btn--${variant} btn--${size}`}>
{children}
</button>
);
}
export default Button;
型定義では variant?: 'primary' | 'secondary' | 'danger' なので、実際の型は undefined を含んだユニオン型です。しかしデフォルト値を書いたことで、関数の中では undefined が除かれた型に絞り込まれます。そのため variant.toUpperCase() のような操作をしても「オブジェクトは undefined である可能性があります」というエラーにはなりません。オプショナルにしただけでデフォルト値を書かない場合は、使う場所ごとに undefined チェックが必要になります。
逆に、? を付け忘れるとどうなるでしょうか。デフォルト値を書いていても型のうえでは必須のままなので、呼び出し側でエラーになります。
type ButtonProps = {
children: ReactNode;
variant: 'primary' | 'secondary'; // ? が無いので必須扱い
};
function Button({ children, variant = 'primary' }: ButtonProps) {
return <button className={`btn btn--${variant}`}>{children}</button>;
}
// エラー: プロパティ 'variant' は型 '{ children: string; }' にありませんが、
// 型 'ButtonProps' では必須です。
<Button>保存</Button>;
デフォルト値を書いたら型定義にも ? を付ける、とセットで覚えておくとよいでしょう。なお、null も受け付けたい props は variant?: 'primary' | 'secondary' | null のように型で明示したうえで、前述の ?? でフォールバックさせます。
バリアントを受け取るボタンを実装してみる
実際に使われる形のコンポーネントで、デフォルト値の効果を確認します。ボタンは props が多くなりがちですが、よく使う値をデフォルトにしておくと呼び出し側が驚くほど短くなります。
import type { ReactNode, MouseEventHandler } from 'react';
type ButtonProps = {
children?: ReactNode;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
type?: 'button' | 'submit' | 'reset';
disabled?: boolean;
onClick?: MouseEventHandler<HTMLButtonElement>;
};
function Button({
children = '送信', // 中身が無ければ「送信」と表示する
variant = 'primary',
size = 'medium',
type = 'button', // HTML の既定値 submit を避ける
disabled = false,
onClick,
}: ButtonProps) {
return (
<button
type={type}
className={`btn btn--${variant} btn--${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
export default Button;
type = 'button' は特に効果が大きいデフォルト値です。HTML の <button> は type を省略すると submit として扱われ、フォームの中に置いたボタンが意図せず送信を発生させます。コンポーネント側で 'button' を既定にしておけば、この事故を仕組みとして防げます。
children にもデフォルト値を設定できます。children は他の props と同じくオブジェクトのプロパティなので、タグの間に何も書かれなければ undefined になり、デフォルト値が使われます。
function App() {
return (
<form>
{/* 「送信」ボタンが type="button" / primary / medium で描画される */}
<Button />
{/* 中身を渡せば children が上書きされる */}
<Button type="submit" variant="primary">
この内容で登録する
</Button>
<Button variant="danger" size="small" onClick={handleDelete}>
削除
</Button>
</form>
);
}
ただし children のデフォルト値には注意点もあります。空配列や false、null を children として渡すケース({items.length && <List />} のような条件付きレンダー)では undefined にならないため、デフォルト値は使われません。表示するものが無いときのフォールバックとして使うなら、children の中身を判定する処理を書いたほうが確実です。
オブジェクト・配列・関数をデフォルト値にすると参照が毎回変わる
デフォルト値に [] や {}、アロー関数を直接書くのは、書き味がよいぶん見落としやすい落とし穴があります。デフォルト値の式はデフォルトが適用されるたびに評価されるため、レンダーのたびに新しいオブジェクトが作られ、毎回異なる参照になるからです。
function TagList({ tags = [] }) {
useEffect(() => {
console.log('tags が変わりました');
// tags を使った処理
}, [tags]); // ← tags は毎回別の配列なので毎レンダー実行される
return <ul>{tags.map((tag) => <li key={tag}>{tag}</li>)}</ul>;
}
親が tags を渡していない状態でこのコンポーネントが再レンダーされると、そのたびに [] が新しく評価されます。[] === [] は false なので、React は依存配列の値が変わったと判断し、useEffect が毎回実行されます。中でデータ取得をしていれば無限ループになりかねません。
React.memo の最適化も無効になる
同じ理由で、React.memo によるメモ化も効かなくなります。React.memo は props を浅い比較(Object.is による同一性の比較)で判定するため、中身が同じ空オブジェクトでも参照が違えば「props が変わった」と見なされます。
function Chart({ options = {}, data }) {
// options は毎回新しいオブジェクト
return <HeavyCanvas options={options} data={data} />;
}
// options の参照が毎回変わるため、memo で包んでも再レンダーされる
const HeavyCanvas = React.memo(function HeavyCanvas({ options, data }) {
// 重い描画処理
return <canvas />;
});
モジュールスコープの定数に切り出す
解決策は単純で、デフォルト値をコンポーネントの外側(モジュールスコープ)で定数として定義し、それを参照することです。モジュールが読み込まれたときに一度だけ作られるので、何度レンダーしても同じ参照が返ります。
// コンポーネントの外で一度だけ作る
const EMPTY_TAGS = [];
function TagList({ tags = EMPTY_TAGS }) {
useEffect(() => {
// tags が実際に変わったときだけ実行される
}, [tags]);
return <ul>{tags.map((tag) => <li key={tag}>{tag}</li>)}</ul>;
}
関数のデフォルト値も同じです。onClose = () => {} と書くと毎回新しい関数が生まれるので、何もしない関数を外に出しておきます。TypeScript では定数にも型を付けておくと、空配列が never[] と推論されるのを防げます。
// 参照が変わらないデフォルト値をモジュールスコープに置く
const EMPTY_TAGS: string[] = [];
const DEFAULT_OPTIONS: ChartOptions = { animated: true };
const noop = () => {};
type ModalProps = {
tags?: string[];
options?: ChartOptions;
onClose?: () => void;
};
function Modal({
tags = EMPTY_TAGS,
options = DEFAULT_OPTIONS,
onClose = noop,
}: ModalProps) {
// ...
}
とはいえ、すべてのデフォルト値を定数に切り出す必要はありません。文字列・数値・真偽値といったプリミティブは値そのもので比較されるため、variant = 'primary' のような書き方で何の問題も起きません。切り出しを検討するのは、そのデフォルト値が依存配列や React.memo のあるコンポーネントに渡る場合だけで十分です。
なお、モジュールスコープに置いた配列やオブジェクトは全レンダー・全インスタンスで共有されます。EMPTY_TAGS.push(...) のように書き換えるとアプリ全体に影響が出るため、あくまで読み取り専用の値として扱ってください。TypeScript なら readonly string[] や as const で守るのも有効です。
defaultProps は関数コンポーネントでは使えない
古い記事やコードでは、defaultProps という静的プロパティにデフォルト値をまとめて書く方法を見かけます。しかし関数コンポーネントの defaultProps は React 19 で削除されました。公式のアップグレードガイドには「関数コンポーネントから defaultProps を削除します。ES6 のデフォルトパラメータを代わりに使用してください」と明記されています。
function Button({ variant, size, children }) {
return <button className={`btn btn--${variant} btn--${size}`}>{children}</button>;
}
// React 19 ではこの指定は反映されない
Button.defaultProps = {
variant: 'primary',
size: 'medium',
};
React 18.3 の時点で「Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.」という非推奨の警告が出るようになり、React 19 で実際に削除されています。React 19 で動かすと variant と size は undefined のままになり、クラス名が btn--undefined になるといった形で表面化します。書き換え先は、この記事の冒頭で紹介した分割代入のデフォルト値です。
一方、クラスコンポーネントの defaultProps は引き続きサポートされます。クラスの props には ES6 のデフォルトパラメータに相当する代替手段がないためです。既存のクラスコンポーネントをそのまま残す場合は、書き換える必要はありません。
import { Component } from 'react';
class Button extends Component {
// クラスコンポーネントでは引き続き有効
static defaultProps = {
variant: 'primary',
size: 'medium',
};
render() {
const { variant, size, children } = this.props;
return <button className={`btn btn--${variant} btn--${size}`}>{children}</button>;
}
}
現状を整理すると次のとおりです。
| 書き方 | 関数コンポーネント | クラスコンポーネント |
|---|---|---|
| 分割代入のデフォルト値 | 推奨される標準の方法 | 使えない(this.props から取り出す際に個別に指定は可能) |
Component.defaultProps | React 19 で削除された | 引き続きサポートされる |
なお、defaultProps と分割代入のデフォルト値には、動作にも違いがありました。defaultProps は React が要素を作る段階で props オブジェクト自体を補完する仕組みだったため、コンポーネントの中では最初からデフォルト値が入った props が見えていました。分割代入のデフォルト値は関数が呼ばれるときに適用されるので、props オブジェクトそのものには variant というキーが存在しません。{...props} で子要素へそのまま展開する設計にしていた場合は、この差が影響することがあります。デフォルト値を含めて渡したいなら、分割代入で受け取った変数を明示的に渡してください。
まとめ
関数コンポーネントで props にデフォルト値を設定する現在の標準は、function Button({ variant = 'primary', size = 'medium' }) { のように引数の分割代入で書く方法です。ここで最も重要なのは、デフォルト値が使われるのは受け取った値が undefined のときだけという点で、null や 0、空文字を渡した場合は置き換わりません。API から null が返ってくる値は ?? でフォールバックさせ、0 や空文字まで潰してしまう || は用途を限定して使います。TypeScript では props の型でオプショナルプロパティ(?)にしたうえでデフォルト値を書くと、呼び出し側で省略でき、コンポーネント内では undefined が除かれた型として扱えます。defaultProps は React 19 で関数コンポーネントから削除されたため新規に使うことはできず、クラスコンポーネントでのみ引き続き利用できます。最後に、デフォルト値に [] や {}、アロー関数を直接書くとレンダーのたびに新しい参照が作られ、useEffect の依存配列や React.memo の比較が壊れます。そうした値はモジュールスコープの定数に切り出して、参照を安定させておきましょう。