Props と State
React アプリケーションにおいて、データの流れを制御する 2 つの重要な概念が Props と State です。 Props は親コンポーネントから子コンポーネントへデータを渡す仕組みであり、State はコンポーネント内部で管理される変化するデータです。 この章では、それぞれの概念を深く理解し、適切に使い分ける方法を学びます。
Props とは
Props(プロップス)は "properties" の略で、親コンポーネントから子コンポーネントへ渡される読み取り専用のデータです。 HTML 要素の属性(attribute)と同じ感覚で、コンポーネントに値を渡すことができます。
Props の最も重要なルールは、子コンポーネント側で Props を直接変更してはならないということです。 Props は常に親から子への「一方向データフロー(one-way data flow)」で流れます。これにより、 データの流れが予測しやすくなり、デバッグが容易になります。
Props は関数の引数と同じです。関数は受け取った引数を変更せず、その値を使って処理を行います。 React コンポーネントも同様に、受け取った Props を使って UI を描画します。
Props の基本的な使い方
Props は、JSX でコンポーネントを呼び出す際に属性として渡します。 子コンポーネントでは、関数の第一引数としてオブジェクト形式で受け取ります。
Props を渡す(親コンポーネント)
import UserCard from './UserCard';
function App() {
return (
<div>
<UserCard name="田中太郎" age={28} role="エンジニア" />
<UserCard name="佐藤花子" age={32} role="デザイナー" />
</div>
);
}
Props を受け取る(子コンポーネント)
// 方法 1: props オブジェクトとして受け取る
function UserCard(props) {
return (
<div className="user-card">
<h2>{props.name}</h2>
<p>年齢: {props.age}歳</p>
<p>役職: {props.role}</p>
</div>
);
}
// 方法 2: 分割代入(推奨)
function UserCard({ name, age, role }) {
return (
<div className="user-card">
<h2>{name}</h2>
<p>年齢: {age}歳</p>
<p>役職: {role}</p>
</div>
);
}
export default UserCard;
分割代入を使うと、props. を省略でき、どの Props を使っているか一目でわかるため推奨されています。
children Props
コンポーネントの開きタグと閉じタグの間に記述した内容は、children という特別な Props として渡されます。
これはレイアウトコンポーネントやラッパーコンポーネントを作る際に非常に便利です。
function Card({ title, children }) {
return (
<div className="card">
<h3 className="card-title">{title}</h3>
<div className="card-body">
{children}
</div>
</div>
);
}
// 使用例
<Card title="お知らせ">
<p>新機能がリリースされました。</p>
<a href="/news">詳細を見る</a>
</Card>
Props の型チェック
コンポーネントが受け取る Props の型を定義しておくと、誤ったデータが渡された場合にコンソールで警告を受け取ることができます。
prop-types パッケージを使って型チェックを行います。
npm install prop-types
import PropTypes from 'prop-types';
function UserCard({ name, age, role }) {
return (
<div className="user-card">
<h2>{name}</h2>
<p>年齢: {age}歳</p>
<p>役職: {role}</p>
</div>
);
}
UserCard.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
role: PropTypes.string,
};
export default UserCard;
TypeScript を使う場合: TypeScript を使えば、PropTypes を使わずにコンパイル時に型チェックが行えます。 大規模なプロジェクトでは TypeScript の利用が主流になっています。
デフォルト Props
Props にデフォルト値を設定しておくと、親コンポーネントが値を渡さなかった場合にその値が使われます。 分割代入のデフォルト値構文を使う方法が現在の推奨パターンです。
function Button({
label = "送信",
variant = "primary",
size = "medium",
disabled = false,
}) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
>
{label}
</button>
);
}
export default Button;
// 使用例
<Button /> // label="送信", variant="primary", size="medium"
<Button label="キャンセル" variant="secondary" /> // size="medium" はデフォルト
State とは
State(ステート)は、コンポーネント内部で管理される変化するデータです。 ユーザーの操作やサーバーからのレスポンスなどによって State が更新されると、React は自動的にそのコンポーネントを再描画(リレンダー)します。
Props が外部から受け取る「読み取り専用」のデータであるのに対し、 State はコンポーネント自身が所有し、自由に変更できるデータです。 ただし、State の変更には React が提供する専用の更新関数を使わなければなりません。
useState フック
関数コンポーネントで State を扱うには、useState フックを使用します。
useState は配列を返し、第 1 要素が現在の State 値、第 2 要素が State を更新する関数です。
import { useState } from 'react';
function Counter() {
// [現在の値, 更新関数] = useState(初期値)
const [count, setCount] = useState(0);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>
+1
</button>
<button onClick={() => setCount(0)}>
リセット
</button>
</div>
);
}
export default Counter;
useState に渡す引数は State の初期値です。
数値、文字列、真偽値、配列、オブジェクトなど、任意の型を使うことができます。
const [name, setName] = useState(''); // 文字列
const [isOpen, setIsOpen] = useState(false); // 真偽値
const [items, setItems] = useState([]); // 空の配列
const [user, setUser] = useState(null); // null
const [form, setForm] = useState({ // オブジェクト
email: '',
password: '',
});
State の更新ルール
1. 直接変更しない
State は必ず更新関数(setter)を通じて変更してください。 変数を直接書き換えても、React はその変更を検知できず、画面が更新されません。
// NG: 直接変更しても再描画されない
count = count + 1;
// OK: 更新関数を使う
setCount(count + 1);
2. 関数型アップデート
新しい State が以前の State に基づく場合は、関数型アップデートを使うのが安全です。 React は State の更新をバッチ処理するため、同じイベントハンドラ内で複数回 setter を呼ぶと、 前の呼び出しの結果が反映されていない可能性があります。
// 値ベースの更新(問題が起きる場合がある)
setCount(count + 1);
setCount(count + 1); // count は同じ値を参照 → 1 しか増えない
// 関数型アップデート(安全)
setCount(prev => prev + 1);
setCount(prev => prev + 1); // 前回の結果を受けて +1 → 合計 2 増える
3. オブジェクト・配列の State は新しい参照を作る
オブジェクトや配列の State を更新するときは、元のデータを変更(ミューテート)するのではなく、 新しいオブジェクト・配列を作成して setter に渡します。
const [user, setUser] = useState({ name: '田中', age: 28 });
// NG: 直接変更
user.name = '佐藤';
setUser(user); // 参照が同じなので React は変更を検知できない
// OK: スプレッド構文で新しいオブジェクトを作成
setUser({ ...user, name: '佐藤' });
const [items, setItems] = useState(['A', 'B', 'C']);
// 追加
setItems([...items, 'D']);
// 削除(filter で新しい配列を生成)
setItems(items.filter(item => item !== 'B'));
// 更新(map で新しい配列を生成)
setItems(items.map(item => item === 'A' ? 'Z' : item));
注意:push()、splice()、sort() などの配列メソッドは元の配列を変更(破壊的操作)するため、
State の更新には使用しないでください。代わりに concat()、filter()、map()、
スプレッド構文 [...] などの非破壊的なメソッドを使いましょう。
Props vs State の違い
Props と State はどちらも再描画のトリガーになりますが、その性質は大きく異なります。 以下の表で違いを整理しましょう。
| 比較項目 | Props | State |
|---|---|---|
| データの所有者 | 親コンポーネント | コンポーネント自身 |
| 変更可能か | 読み取り専用(変更不可) | 更新関数で変更可能 |
| データの方向 | 親 → 子(一方向) | コンポーネント内部 |
| 用途 | コンポーネントの設定・カスタマイズ | ユーザー操作等で変化するデータ |
| 変更時の動作 | 子コンポーネントが再描画される | そのコンポーネントが再描画される |
| 例え | 関数の引数 | 関数内のローカル変数 |
State のリフトアップ
複数の子コンポーネントが同じデータを共有する必要がある場合、 そのデータ(State)を共通の親コンポーネントに移動させます。 これを 「State のリフトアップ(Lifting State Up)」と呼びます。
次の例では、温度入力コンポーネントが摂氏と華氏の 2 つあり、 一方の入力が変わるともう一方もリアルタイムに同期する仕組みを実装しています。
import { useState } from 'react';
// 変換関数
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return celsius * 9 / 5 + 32;
}
// 子コンポーネント:温度入力
function TemperatureInput({ scale, temperature, onTemperatureChange }) {
const scaleNames = { c: '摂氏 (°C)', f: '華氏 (°F)' };
return (
<fieldset>
<legend>{scaleNames[scale]} で入力:</legend>
<input
type="number"
value={temperature}
onChange={(e) => onTemperatureChange(e.target.value)}
/>
</fieldset>
);
}
// 親コンポーネント:State をここで管理
function TemperatureConverter() {
const [temperature, setTemperature] = useState('');
const [scale, setScale] = useState('c');
const celsius = scale === 'f'
? toCelsius(parseFloat(temperature))
: temperature;
const fahrenheit = scale === 'c'
? toFahrenheit(parseFloat(temperature))
: temperature;
return (
<div>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={(val) => { setTemperature(val); setScale('c'); }}
/>
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={(val) => { setTemperature(val); setScale('f'); }}
/>
</div>
);
}
export default TemperatureConverter;
この例のポイントは、temperature と scale という State を親コンポーネントで一元管理し、
子コンポーネントには Props と更新用のコールバック関数を渡していることです。
これにより、2 つの入力が常に同期した状態を保てます。
リフトアップのタイミング: 2 つ以上のコンポーネントが同じデータを必要とする場合、それぞれの State ではなく、 最も近い共通の親に State を持たせましょう。「State は必要とされる場所のできるだけ近くに置く」が原則です。
不変性が重要な理由
React では State を更新する際に不変性(immutability)を守ることが極めて重要です。 不変性とは、既存のデータを直接変更せず、新しいデータを作成してから置き換えることを指します。
React のレンダリング最適化
React は State が変更されたかどうかを参照の比較(Object.is)で判定します。 つまり、オブジェクトの中身が変わっていても、参照(メモリアドレス)が同じなら「変更なし」と判断されてしまいます。
const [todos, setTodos] = useState([
{ id: 1, text: '買い物', done: false },
{ id: 2, text: '掃除', done: false },
]);
// NG: 元の配列を直接変更
todos[0].done = true;
setTodos(todos); // 参照が同じ → 再描画されない可能性がある
// OK: 新しい配列を作成
setTodos(todos.map(todo =>
todo.id === 1 ? { ...todo, done: true } : todo
)); // 新しい参照 → 正しく再描画される
不変性のメリットまとめ
- 変更検知が高速:参照の比較だけで済むため、深い比較が不要になる
- 予期しない副作用を防止:元のデータが変わらないため、他の箇所に影響しない
- デバッグが容易:以前の State と現在の State を比較できる(タイムトラベルデバッグ)
- React.memo などの最適化が有効に機能する:メモ化が正しく動作する
覚えておくべきこと:
React では、State を更新するときは常に「新しいオブジェクト・配列を作って setter に渡す」というパターンを守りましょう。
push() や直接代入ではなく、スプレッド構文 {...obj} や [...arr]、
map()、filter() を使うのが React 開発の基本です。