ReactにおけるPropsとStateの管理

2025-08-07

はじめに

Reactコンポーネントの核心概念であるProps(プロパティ)とState(状態)について、初心者向けに徹底解説します。これらの概念を理解することで、動的でインタラクティブなアプリケーションを構築できるようになります。

PropsとStateの基本理解

Propsとは?

Props(プロパティ)は、親コンポーネントから子コンポーネントへデータを受け渡すための仕組みです。コンポーネント同士が情報をやり取りする際の基本的な手段であり、UIの再利用性や構造の明確さを支える重要な要素です。

Propsの大きな特徴の一つは読み取り専用である点です。子コンポーネントは受け取ったPropsを変更することができず、あくまで「与えられた値を使って表示や処理を行う」ことに専念します。これにより、データの流れが予測しやすくなり、バグの発生を抑える設計になっています。

また、Propsは単方向のデータフローを持ち、常に親コンポーネントから子コンポーネントへと一方通行で渡されます。子から親へ直接データを戻すことはできず、必要な場合はコールバック関数をPropsとして渡すことで間接的にやり取りします。この一方向の流れが、アプリケーション全体の状態管理をシンプルに保つポイントになります。

// 親コンポーネント
function ParentComponent() {
  return <ChildComponent name="山田太郎" age={25} />;
}

// 子コンポーネント
function ChildComponent(props) {
  return (
    <div>
      <p>名前: {props.name}</p>
      <p>年齢: {props.age}</p>
    </div>
  );
}

さらに、Propsにはさまざまなデータ型を渡すことが可能です。文字列や数値といった基本的な型だけでなく、オブジェクトや配列、さらには関数そのものも渡すことができます。これにより、単なるデータ表示だけでなく、イベント処理やロジックの共有といった柔軟なコンポーネント設計が実現できます。

Stateとは?

ReactにおけるStateとは、コンポーネント内部で管理される状態データのことを指します。Stateはそのコンポーネントに固有のものであり、同じコンポーネントを複数使用した場合でも、それぞれが独立したStateを持ちます。そのため、あるコンポーネントのStateの変更が他のコンポーネントに直接影響を与えることはありません。

また、Stateは動的に変更可能であり、クラスコンポーネントではsetStateメソッド、関数コンポーネントではuseStateフックを用いることで更新されます。これにより、ユーザーの操作や非同期処理の結果に応じて、コンポーネントの表示内容を柔軟に変化させることができます。

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0); // Stateの宣言

  return (
    <div>
      <p>現在のカウント: {count}</p>
      <button onClick={() => setCount(count + 1)>増やす</button>
    </div>
  );
}

さらに重要な特徴として、Stateの変更はコンポーネントの再レンダリングを引き起こします。つまり、Stateが更新されるとReactはそのコンポーネントを再描画し、最新のStateに基づいたUIを反映します。この仕組みによって、アプリケーションの状態と画面表示が常に同期されるようになっています。

Propsの詳細解説

Propsの受け渡し方法

関数型コンポーネントでのProps

ReactにおけるPropsの受け取り方の違いを示しています。Welcomeコンポーネントは親から渡されたnameを使って挨拶文を表示します。1つ目はpropsオブジェクトとして受け取り、props.nameで値にアクセスする書き方です。

// 方法1: propsオブジェクトを使用
function Welcome(props) {
  return <h1>こんにちは、{props.name}さん!</h1>;
}

// 方法2: 分割代入を使用(推奨)
function Welcome({ name }) {
  return <h1>こんにちは、{name}さん!</h1>;
}

// 使用例
<Welcome name="佐藤花子" />

2つ目は分割代入を使い、引数の段階でnameを直接取り出す方法で、より簡潔に書けるため一般的に推奨されます。使用例ではname="佐藤花子"が渡され、画面にはその名前を含んだメッセージが表示されます。

クラス型コンポーネントでのProps

クラスコンポーネントでPropsを扱う基本例です。WelcomeReact.Componentを継承したクラスとして定義され、renderメソッド内でUIを返します。

class Welcome extends React.Component {
  render() {
    return <h1>こんにちは、{this.props.name}さん!</h1>;
  }
}

// 使用例
<Welcome name="田中一郎" />

関数コンポーネントと異なり、Propsにはthis.propsを通してアクセスする点が特徴です。ここでは親から渡されたnamethis.props.nameで取得し、挨拶文として表示しています。使用例ではname="田中一郎"が渡され、その値が画面に反映されます。

デフォルトPropsの設定

Propsが渡されなかった場合のデフォルト値を設定できます。Welcomeコンポーネントは分割代入でnameを受け取り、その値を使って挨拶を表示します。Welcome.defaultPropsname: 'ゲスト'を設定することで、親からnameが渡されなかった場合でも、自動的に「ゲスト」という値が補われます。

function Welcome({ name }) {
  return <h1>こんにちは、{name}さん!</h1>;
}

Welcome.defaultProps = {
  name: 'ゲスト'
};

// nameが未指定の場合
<Welcome /> // "こんにちは、ゲストさん!"と表示

そのため、使用例のように<Welcome />とだけ書いても、nameは未定義にならず「こんにちは、ゲストさん!」と表示されます。これにより、Propsが省略された場合でも安全にコンポーネントを動作させることができます。

子供要素としてのProps(children)

コンポーネントタグの間に記述した内容はchildrenとして渡されます。Cardコンポーネントは引数でchildrenを受け取り、その中身をdiv要素内にそのまま表示します。childrenとは、コンポーネントの開始タグと終了タグの間に書かれた要素やテキストを指します。

function Card({ children }) {
  return <div className="card">{children}</div>;
}

// 使用例
<Card>
  <h2>タイトル</h2>
  <p>コンテンツ</p>
</Card>

使用例では、<Card>タグの中にh2pが記述されており、それらがchildrenとして渡され、カードの中身として描画されます。この仕組みによって、レイアウトだけを共通化しつつ、中身を柔軟に差し替えられる再利用性の高いコンポーネントを作ることができます。

Propsの型チェック(PropTypes)

ReactコンポーネントにおけるPropsの型チェックとデフォルト値の設定方法を示しています。UserProfilenameagehobbiesを受け取る関数コンポーネントで、propTypesを使って各Propsの型を定義しています。nameは必須の文字列、ageは数値、hobbiesは文字列の配列であることを指定しており、開発時に不正な型が渡された場合に警告が表示されます。

import PropTypes from 'prop-types';

function UserProfile({ name, age, hobbies }) {
  // ...
}

UserProfile.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number,
  hobbies: PropTypes.arrayOf(PropTypes.string)
};

UserProfile.defaultProps = {
  age: 20,
  hobbies: []
};

さらにdefaultPropsを設定することで、ageが未指定なら20、hobbiesが未指定なら空配列が自動的に使われます。これにより、Propsの受け取りに安全性と一貫性を持たせることができます。

Stateの詳細解説

関数型コンポーネントでのState(useStateフック)

React 16.8で導入されたHooks APIを使用します。ReactのuseStateフックを使った状態管理の基本例です。Counterコンポーネントでは、useState(0)によりcount(現在の値)とsetCount(更新関数)を取得し、初期値を0に設定しています。

import { useState } from 'react';

function Counter() {
  // count: 現在の状態値
  // setCount: 状態を更新する関数
  const [count, setCount] = useState(0); // 初期値0

  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)>増やす</button>
      <button onClick={() => setCount(count - 1)>減らす</button>
      <button onClick={() => setCount(0)>リセット</button>
    </div>
  );
}

画面には現在のカウントが表示され、3つのボタンで値を操作できます。「増やす」はcount + 1、「減らす」はcount - 1、「リセット」は0をセットします。setCountが呼ばれるたびにStateが更新され、コンポーネントが再レンダリングされて最新の値が画面に反映される仕組みになっています。

複数のStateの管理

このコードは、Reactでフォーム入力をStateで管理する「制御コンポーネント」の例です。useStateを使ってnameemailageそれぞれの状態を定義し、入力値とStateを常に同期させています。

function Form() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [age, setAge] = useState(20);

  return (
    <form>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="名前"
      />
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="メールアドレス"
      />
      <input
        type="number"
        value={age}
        onChange={(e) => setAge(Number(e.target.value))}
        placeholder="年齢"
      />
    </form>
  );
}

input要素のvalueにはStateの値を設定し、onChangeイベントで入力内容を受け取り、対応する更新関数でStateを書き換えます。これにより、入力値は常にReact側で管理され、画面表示とデータが一致します。なお、ageは数値として扱うため、Numberで変換している点もポイントです。

クラス型コンポーネントでのState

このコードは、クラスコンポーネントでStateを管理するカウンターの例です。constructorで初期Stateとしてcount: 0を設定し、this.stateで状態を保持します。値の更新にはthis.setStateを使用し、incrementdecrementresetメソッドでそれぞれカウントの増減や初期化を行っています。

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };

  decrement = () => {
    this.setState({ count: this.state.count - 1 });
  };

  reset = () => {
    this.setState({ count: 0 });
  };

  render() {
    return (
      <div>
        <p>カウント: {this.state.count}</p>
        <button onClick={this.increment}>増やす</button>
        <button onClick={this.decrement}>減らす</button>
        <button onClick={this.reset}>リセット</button>
      </div>
    );
  }
}

renderメソッドでは現在のcountを表示し、各ボタンのクリックイベントで対応するメソッドが呼ばれる仕組みです。setStateが実行されるとコンポーネントが再レンダリングされ、最新の状態が画面に反映されます。

State更新の注意点

関数型コンポーネントでの注意

  1. 直接変更しない

count = count + 1のように直接変数を変更しても、ReactのStateは更新されず再レンダリングも発生しません。一方でsetCount(count + 1)を使うと、ReactがStateの変更を検知し、コンポーネントが再描画されて画面に反映されます。つまり、Stateは直接変更せず専用の更新関数で変更する必要があります。

   // 誤り
   count = count + 1;
   // 正しい
   setCount(count + 1);
  1. 前の状態に依存する更新

前のState値を使って安全に更新する書き方です。prevCountには更新前の最新の値が入り、それに対して+1しているため、複数回の連続更新でも正しい値が保証されます。

   setCount(prevCount => prevCount + 1);

クラス型コンポーネントでの注意

  1. 直接stateを変更しない

this.state.count = ...のように直接書き換えてもReactは変更を検知できず、再レンダリングも行われません。一方でthis.setStateを使うとStateの更新がReactに通知され、コンポーネントが再描画されて画面に反映されます。つまり、Stateは必ずsetStateで更新する必要があります。

   // 誤り(再レンダリングが発生しない)
   this.state.count = this.state.count + 1;
   // 正しい
   this.setState({ count: this.state.count + 1 });
  1. state更新は非同期

this.state.countを直接参照すると、非同期更新の影響で最新の値ではない可能性があります。そのため、誤りの書き方では連続更新時にズレが発生することがあります。一方で正しい方法ではprevStateを使い、常に最新のStateを基準にしてcountを更新します。これにより、複数回の更新でも正確な値が保証されます。

   // 誤り(this.state.countが最新でない可能性)
   this.setState({ count: this.state.count + 1 });
   // 正しい
   this.setState((prevState) => ({
     count: prevState.count + 1
   }));

オブジェクト型のState

userというStateに名前・メール・年齢をまとめて管理しています。handleChangeでは入力イベントからnamevalueを取得し、setUserで更新します。その際...prevUserで既存のStateを展開し、変更されたプロパティだけを[name]: valueで上書きしています。これにより、他の項目を保持したまま一部だけ更新できます。1つのStateで複数入力を効率的に管理するパターンです。

function UserForm() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 20
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setUser(prevUser => ({
      ...prevUser, // 既存のstateを展開
      [name]: value // 変更されたプロパティを更新
    }));
  };

  return (
    <form>
      <input
        name="name"
        value={user.name}
        onChange={handleChange}
        placeholder="名前"
      />
      <input
        name="email"
        value={user.email}
        onChange={handleChange}
        placeholder="メールアドレス"
      />
      <input
        name="age"
        type="number"
        value={user.age}
        onChange={handleChange}
        placeholder="年齢"
      />
    </form>
  );
}

デバッグ技法

デバッグには適宜console.logでデータを確認します。

Propsのデバッグ

ChildComponentpropsを引数として受け取り、console.logで渡されたデータを出力しています。これにより、親コンポーネントからどのような値が渡されているかをデバッグ目的で確認できます。実際のUIではpropsを使って表示内容を変えることができ、コンポーネント間のデータ受け渡しの理解に役立つコードです。

function ChildComponent(props) {
  console.log('Received props:', props); // コンソールで確認
  return <div>...</div>;
}

Stateのデバッグ

useStateで管理しているstateが更新されるたびに、useEffect内の処理が実行されます。第二引数の依存配列に[state]を指定しているため、stateが変更されたときだけconsole.logが実行され、「State changed:」というログが出力されます。これにより、Stateの変化に応じた処理(ログ出力やAPI通信など)を制御できます。

function MyComponent() {
  const [state, setState] = useState(/* ... */);

  useEffect(() => {
    console.log('State changed:', state);
  }, [state]); // stateが変更されるたびにログ出力

  // ...
}

PropsとStateの重要なポイント

Reactにおける「Props」と「State」は、コンポーネント設計の中核を担う重要な概念です。それぞれの役割と使い分けを正しく理解することで、保守性が高く、バグの少ないアプリケーションを構築できます。

PropsとStateの使い分けには明確な原則があります。データが親コンポーネントから子コンポーネントへ受け渡される場合はPropsを使い、コンポーネント自身が管理・変更する必要があるデータはStateとして扱うべきです。このルールを守ることで、データの責任範囲が明確になり、コンポーネントの再利用性や可読性が向上します。

また、Reactでは「不変性(イミュータビリティ)」の考え方が非常に重要です。Propsは前述の通り変更してはいけませんが、Stateについても直接書き換えるのではなく、専用の更新関数(例:setStateuseStateの更新関数)を使って新しい値として更新する必要があります。これにより、Reactは変更を正しく検知し、効率的に再レンダリングを行うことができます。

まとめ

まとめると、Propsは「外から与えられる設定値」、Stateは「内部で変化する状態」として役割が分かれており、この2つを適切に使い分けることがReact開発の基本となります。

PropsとStateを適切に使い分けることで、Reactアプリケーションのデータフローを明確に保つことができます。次に学ぶイベント処理やライフサイクルメソッドと組み合わせることで、さらにインタラクティブなコンポーネントを作成できるようになります。


🟢 初級問題(問題1〜3)

Props の基本的な渡し方と受け取り方、State の初期化と更新を練習します。

問題1:Propsでデータを受け取って表示しよう

【問題】
以下の条件をすべて満たすコンポーネントを作成してください。

  • コンポーネント名は UserCard とする
  • Props として name(文字列)・age(数値)・job(文字列)を受け取る
  • 分割代入を使って Props を受け取ること
  • 以下の3つのインスタンスを並べて表示する親コンポーネント App も作成する
<UserCard name="田中太郎" age={28} job="エンジニア" />
<UserCard name="鈴木花子" age={24} job="デザイナー" />
<UserCard name="佐藤次郎" age={32} job="マネージャー" />

【期待する表示(1枚分)】

田中太郎(28歳)
エンジニア

問題2:デフォルトPropsを設定しよう

【問題】
以下の条件をすべて満たす ProfileBadge コンポーネントを作成してください。

  • Props として username(文字列)・level(数値)・role(文字列)を受け取る
  • defaultProps で以下のデフォルト値を設定する:username="ゲスト"level={1}role="一般ユーザー"
  • 以下の3パターンで動作確認する
<ProfileBadge username="管理者" level={99} role="admin" /> {/* 全部指定 */}
<ProfileBadge username="田中" />                           {/* levelとroleを省略 */}
<ProfileBadge />                                           {/* すべて省略 */}

【期待する表示】

管理者 | Lv.99 | admin
田中 | Lv.1 | 一般ユーザー
ゲスト | Lv.1 | 一般ユーザー

問題3:useStateで切り替えを実装しよう

【問題】
以下の条件をすべて満たす ToggleCard コンポーネントを作成してください。

  • State として isOpen(初期値: false)を useState で管理する
  • ボタンをクリックするたびに isOpentrue / false で切り替わる(!isOpen を使うこと)
  • isOpentrue のとき:ボタンに「閉じる」と表示し、<p>コンテンツが表示されています!</p> を表示する
  • isOpenfalse のとき:ボタンに「開く」と表示し、コンテンツは非表示にする

【期待する表示(初期状態)】

[開く]
【ボタンを1回クリック後】
[閉じる]
コンテンツが表示されています!

🟡 中級問題(問題4〜9)

Props で関数を渡す・children を使う・オブジェクト State を更新するなど、実践的なパターンに取り組みます。

問題4:Propsで関数を渡して子から親を更新しよう

【問題】
以下の条件をすべて満たすプログラムを作成してください。

  • 親コンポーネント Appcount State(初期値: 0)を管理する
  • 子コンポーネント CounterButtonlabel(文字列)と onClick(関数)を Props として受け取り、ボタンを表示するだけにする
  • App の中で CounterButton を使い、「+1」「−1」「リセット」の3ボタンを実装する
  • 現在の countApp 内で表示する

【期待する表示(初期状態)】

カウント: 0
[+1] [−1] [リセット]

問題5:childrenを使った汎用コンポーネントを作ろう

【問題】
以下の条件をすべて満たすプログラムを作成してください。

  • Panel コンポーネントを作成する。title(文字列)と children を Props として受け取る
  • Paneltitle<h3> で表示し、children をその下に表示する
  • 以下のように Panel を3回使い、それぞれ異なる内容を入れること
<Panel title="お知らせ">
  <p>新機能がリリースされました。</p>
</Panel>

<Panel title="注意事項">
  <ul>
    <li>定期メンテナンス: 毎週火曜日</li>
    <li>問い合わせ: support@example.com</li>
  </ul>
</Panel>

<Panel title="統計">
  <p>本日の訪問者: 1,234人</p>
  <p>累計ユーザー: 98,765人</p>
</Panel>

問題6:オブジェクトStateをスプレッド構文で更新しよう

【問題】
以下の条件をすべて満たす ProfileEditor コンポーネントを作成してください。

  • 以下のオブジェクトを初期値とする profile State を useState で管理する
{ name: "山田太郎", bio: "Reactを勉強中", location: "東京" }
  • 各フィールドの inputonChange はすべて同一の handleChange 関数で処理する(e.target.name を使うこと)
  • State 更新は必ずスプレッド構文(...prevProfile)を使うこと
  • 入力欄の下にプレビューとして現在の profile の内容をリアルタイムに表示する

【期待する表示(初期状態)】

名前: [山田太郎          ]
自己紹介: [Reactを勉強中  ]
所在地: [東京            ]

【プレビュー】
山田太郎 / 東京
Reactを勉強中

問題7:配列Stateにアイテムを追加・削除しよう

【問題】
以下の条件をすべて満たす ShoppingList コンポーネントを作成してください。

  • State として以下を管理する:items(配列、初期値: ["牛乳", "卵", "パン"])・inputValue(文字列、初期値: ""
  • 「追加」ボタン:inputValue が空でない場合のみ items に追加し、inputValue を空に戻す
  • 各アイテムの横に「削除」ボタンを置き、クリックすると該当アイテムを filter() で除去する
  • State の更新は直接変更せず、filter() や スプレッド構文 [...items, 新要素] を使うこと

【期待する表示(初期状態)】

[入力欄         ] [追加]
• 牛乳 [削除]
• 卵   [削除]
• パン [削除]

問題8:PropsとStateを組み合わせたカード表示を作ろう

【問題】
以下の条件をすべて満たすプログラムを作成してください。

  • 子コンポーネント ProductCardnamepriceonAddToCart を Props として受け取り、商品名・価格・「カートに追加」ボタンを表示する
  • 親コンポーネント Shopcart(配列 State、初期値: [])を管理し、addToCart(name) 関数で namecart に追加する
  • 以下の3商品を ProductCard で並べる
{ name: "リンゴ",    price: 150 }
{ name: "バナナ",   price: 80  }
{ name: "みかん",   price: 120 }
  • ページ下部に「カートの中身: 〇〇, 〇〇, …(〇個)」と表示する

【期待する表示(リンゴを2回、バナナを1回押した後)】

カートの中身: リンゴ, リンゴ, バナナ(3個)

問題9:前の状態に依存するState更新をマスターしよう

【問題】
以下の条件をすべて満たす VoteCounter コンポーネントを作成してください。

  • State として votes(数値、初期値: 0)を管理する
  • 「+3票まとめて」ボタン:setVotes3回連続で呼び出すことで3票増やす。関数形式prev => prev + 1)を使うこと
  • 「+3票まとめて(誤った方法)」ボタン:setVotes(votes + 1)3回連続で呼び出す(実際は1しか増えないことを確認するため)
  • 「リセット」ボタン:votes0 に戻す
  • 現在の票数を表示する

【期待する動作】

現在の票数: 0
[+3票まとめて(正しい)] [+3票まとめて(誤り)] [リセット]

→ 正しいボタンを押す: 3票増える(3, 6, 9...)
→ 誤りボタンを押す:   1票しか増えない(バグの確認)

🔴 上級問題(問題10〜12)

Props と State を組み合わせた複合的な設計・複数コンポーネント間のデータフロー・実践的なフォーム実装に挑戦します。

問題10:複数コンポーネント間でStateを共有しよう(State のリフトアップ)

【問題】
以下の条件をすべて満たすプログラムを作成してください。

  • FilterInput(子):テキスト入力欄を表示する。valueonChange を Props で受け取る
  • UserList(子):ユーザーの一覧を表示する。users(配列)を Props で受け取り map() で表示する
  • App(親):query State(初期値: "")を管理し、FilterInput に渡す。以下のデータを query でフィルタリングして UserList に渡す
const users = [
  { id: 1, name: "田中太郎",   dept: "開発部" },
  { id: 2, name: "鈴木花子",   dept: "デザイン部" },
  { id: 3, name: "佐藤次郎",   dept: "開発部" },
  { id: 4, name: "山田三郎",   dept: "営業部" },
  { id: 5, name: "伊藤美咲",   dept: "デザイン部" },
];
  • フィルタリングは name または deptquery を含むものとする(大文字小文字を区別しない)
  • State は App だけが持ち、子コンポーネントは Props の受け渡しだけで動作させる(これを「State のリフトアップ」と呼ぶ)

【期待する動作】

検索: [開発          ]

田中太郎 — 開発部
佐藤次郎 — 開発部

問題11:バリデーション付きフォームを実装しよう

【問題】
以下の条件をすべて満たす SignupForm コンポーネントを作成してください。

  • State として以下を管理する
const [form,   setForm]   = useState({ username: '', email: '', password: '' });
const [errors, setErrors] = useState({});
  • 「送信」ボタンを押したとき、以下のバリデーションを実行し、エラーがあれば errors State に格納して各フィールドの下に赤字で表示する
フィールドルールエラーメッセージ
username3文字以上“ユーザー名は3文字以上で入力してください”
email@ を含む“正しいメールアドレスを入力してください”
password8文字以上“パスワードは8文字以上で入力してください”
  • すべてのバリデーションが通った場合のみ alert("送信完了!") を実行し、フォームと errors をリセットする

問題12:TodoアプリでProps・Stateを総合的に使おう

【問題】
以下のコンポーネント設計でTodoアプリを実装してください。

  • TodoInput(子):入力欄と「追加」ボタンを表示。valueonChangeonAdd を Props で受け取る
  • TodoItem(子):1件のTodoを表示。todo(オブジェクト)・onToggleonDelete を Props で受け取る。完了時は取り消し線を表示する
  • TodoApp(親):以下の State を管理する
const [todos, setTodos] = useState([
  { id: 1, text: "牛乳を買う",        done: false },
  { id: 2, text: "Reactを練習する",   done: false },
  { id: 3, text: "メールを確認する",  done: true  },
]);
const [input, setInput] = useState("");
  • 追加input が空でなければ新しいTodoを追加。idDate.now() を使う
  • 完了切り替え:対象のTodoの done を反転させる(map() を使うこと)
  • 削除:対象のTodoを filter() で除去する
  • フッター:「残り〇件 / 全〇件」と未完了数・総数を表示する

【期待する表示(初期状態)】

[入力欄                ] [追加]
☐ 牛乳を買う         [削除]
☐ Reactを練習する    [削除]
☑ ~~メールを確認する~~ [削除]
残り2件 / 全3件

📘 解説(全12問)

解説1:Propsでデータを受け取って表示しよう

【解答例】

// 分割代入で Props を受け取る
function UserCard({ name, age, job }) {
  return (
    <div className="user-card">
      <p><strong>{name}</strong>({age}歳)</p>
      <p>{job}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <UserCard name="田中太郎" age={28} job="エンジニア" />
      <UserCard name="鈴木花子" age={24} job="デザイナー" />
      <UserCard name="佐藤次郎" age={32} job="マネージャー" />
    </div>
  );
}

【解説】

  • 分割代入 ({ name, age, job }) を使うと、受け取る Props が一目でわかり、コード内での記述も短くなる
  • 数値の Props は age={28} のように波括弧で渡す。age="28" と書くと文字列になってしまうので注意
  • 同じコンポーネントを繰り返し使うことで、コードの再利用性が実感できる

解説2:デフォルトPropsを設定しよう

【解答例】

function ProfileBadge({ username, level, role }) {
  return (
    <p>{username} | Lv.{level} | {role}</p>
  );
}

ProfileBadge.defaultProps = {
  username: "ゲスト",
  level:    1,
  role:     "一般ユーザー",
};

function App() {
  return (
    <div>
      <ProfileBadge username="管理者" level={99} role="admin" />
      <ProfileBadge username="田中" />
      <ProfileBadge />
    </div>
  );
}

【解説】

  • defaultProps はコンポーネント定義の外側に書く。Props が渡されたときはそちらが優先されるため、デフォルト値が上書きされる
  • 関数型コンポーネントでは function ProfileBadge({ username = "ゲスト", level = 1 }) のようにデフォルト引数でも同じことができる
  • defaultProps はクラス型でも同じ書き方で動作する

解説3:useStateで切り替えを実装しよう

【解答例】

import { useState } from 'react';

function ToggleCard() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>
        {isOpen ? "閉じる" : "開く"}
      </button>
      {isOpen && <p>コンテンツが表示されています!</p>}
    </div>
  );
}

【解説】

  • setIsOpen(!isOpen) は「現在の値を反転させる」操作で、トグル処理の定番パターン
  • ボタンのテキストも三項演算子で切り替えることで、State の値とUIが常に同期する
  • {isOpen && <p>...</p>}isOpentrue のときだけ要素を描画するショートサーキット評価

解説4:Propsで関数を渡して子から親を更新しよう

【解答例】

import { useState } from 'react';

// 子:Props でラベルとコールバック関数を受け取るだけ
function CounterButton({ label, onClick }) {
  return <button onClick={onClick}>{label}</button>;
}

// 親:State を管理し、子に関数を渡す
function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>カウント: {count}</p>
      <CounterButton label="+1"    onClick={() => setCount(count + 1)} />
      <CounterButton label="−1"   onClick={() => setCount(count - 1)} />
      <CounterButton label="リセット" onClick={() => setCount(0)} />
    </div>
  );
}

【解説】

  • 子コンポーネントは「表示と呼び出しだけ」に徹し、ロジックは親が持つ設計が基本
  • 関数を Props として渡すことで「子のボタン → 親の State 更新」という単方向データフローが実現できる
  • CounterButton のように汎用化することで、同じパターンのボタンを何度でも再利用できる

解説5:childrenを使った汎用コンポーネントを作ろう

【解答例】

function Panel({ title, children }) {
  return (
    <div className="panel">
      <h3>{title}</h3>
      <div className="panel-body">{children}</div>
    </div>
  );
}

function App() {
  return (
    <div>
      <Panel title="お知らせ">
        <p>新機能がリリースされました。</p>
      </Panel>

      <Panel title="注意事項">
        <ul>
          <li>定期メンテナンス: 毎週火曜日</li>
          <li>問い合わせ: support@example.com</li>
        </ul>
      </Panel>

      <Panel title="統計">
        <p>本日の訪問者: 1,234人</p>
        <p>累計ユーザー: 98,765人</p>
      </Panel>
    </div>
  );
}

【解説】

  • children を使うと、コンポーネントタグの中に何でも入れられる「スロット」のような機能を実現できる
  • Panel 自体は見た目の枠だけを担当し、中身は呼び出し側が自由に決められるため汎用性が高い
  • title のような追加 Props と children を組み合わせることで、柔軟なラッパーコンポーネントが作れる

解説6:オブジェクトStateをスプレッド構文で更新しよう

【解答例】

import { useState } from 'react';

function ProfileEditor() {
  const [profile, setProfile] = useState({
    name:     "山田太郎",
    bio:      "Reactを勉強中",
    location: "東京",
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setProfile(prev => ({
      ...prev,    // 他のフィールドをコピー
      [name]: value // 変更したフィールドだけ上書き
    }));
  };

  return (
    <div>
      <input name="name"     value={profile.name}     onChange={handleChange} placeholder="名前" />
      <input name="bio"      value={profile.bio}      onChange={handleChange} placeholder="自己紹介" />
      <input name="location" value={profile.location} onChange={handleChange} placeholder="所在地" />

      <div>
        <h4>【プレビュー】</h4>
        <p>{profile.name} / {profile.location}</p>
        <p>{profile.bio}</p>
      </div>
    </div>
  );
}

【解説】

  • ...prev を忘れると、name を更新したときに biolocation が消える。スプレッド構文は必須
  • [name]: value[] は動的プロパティ名。変数 name の値(例: "bio")がそのままキー名になる
  • 1つの handleChange 関数で全フィールドを処理できるため、フィールドが増えても関数を増やす必要がない

解説7:配列Stateにアイテムを追加・削除しよう

【解答例】

import { useState } from 'react';

function ShoppingList() {
  const [items,      setItems]      = useState(["牛乳", "卵", "パン"]);
  const [inputValue, setInputValue] = useState("");

  const handleAdd = () => {
    if (inputValue.trim() === "") return; // 空なら追加しない
    setItems([...items, inputValue.trim()]); // スプレッドで新要素を追加
    setInputValue(""); // 入力欄をリセット
  };

  const handleDelete = (index) => {
    setItems(items.filter((_, i) => i !== index)); // 該当インデックスを除外
  };

  return (
    <div>
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="商品を入力..."
      />
      <button onClick={handleAdd}>追加</button>
      <ul>
        {items.map((item, index) => (
          <li key={index}>
            {item}
            <button onClick={() => handleDelete(index)}>削除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

【解説】

  • 配列を直接変更(items.push() など)せず、[...items, 新要素] で新しい配列を作るのが React の原則(不変性の維持)
  • 削除は filter() で「残したい要素だけを集めた新しい配列」を作る
  • inputValue.trim() で前後の空白を除去してから空チェックする

解説8:PropsとStateを組み合わせたカード表示を作ろう

【解答例】

import { useState } from 'react';

function ProductCard({ name, price, onAddToCart }) {
  return (
    <div className="product-card">
      <p>{name} — ¥{price}</p>
      <button onClick={() => onAddToCart(name)}>カートに追加</button>
    </div>
  );
}

function Shop() {
  const [cart, setCart] = useState([]);

  const addToCart = (name) => {
    setCart(prev => [...prev, name]);
  };

  const products = [
    { name: "リンゴ", price: 150 },
    { name: "バナナ", price: 80  },
    { name: "みかん", price: 120 },
  ];

  return (
    <div>
      {products.map((p) => (
        <ProductCard
          key={p.name}
          name={p.name}
          price={p.price}
          onAddToCart={addToCart}
        />
      ))}
      <p>カートの中身: {cart.join(", ")}({cart.length}個)</p>
    </div>
  );
}

【解説】

  • State(cart)は Shop(親)が管理し、ProductCard(子)は表示とコールバックの呼び出しだけを担当する
  • onAddToCart という関数 Props を渡すことで「子のボタン → 親の State 更新」のデータフローが完成する
  • cart.join(", ") で配列を文字列に変換して表示する

解説9:前の状態に依存するState更新をマスターしよう

【解答例】

import { useState } from 'react';

function VoteCounter() {
  const [votes, setVotes] = useState(0);

  const addThreeCorrect = () => {
    // 関数形式:常に最新の状態を参照する
    setVotes(prev => prev + 1);
    setVotes(prev => prev + 1);
    setVotes(prev => prev + 1);
  };

  const addThreeWrong = () => {
    // 直接参照:votes が古い値のまま3回実行されるので1しか増えない
    setVotes(votes + 1);
    setVotes(votes + 1);
    setVotes(votes + 1);
  };

  return (
    <div>
      <p>現在の票数: {votes}</p>
      <button onClick={addThreeCorrect}>+3票まとめて(正しい)</button>
      <button onClick={addThreeWrong}>+3票まとめて(誤り)</button>
      <button onClick={() => setVotes(0)}>リセット</button>
    </div>
  );
}

【解説】

  • React の State 更新は非同期のためバッチ処理される。同じレンダリング内で setVotes(votes + 1) を3回呼んでも、votes はすべて同じ古い値を参照するため結果は +1 にしかならない
  • 関数形式 prev => prev + 1 はキューに積まれた直前の更新結果を参照するため、3回呼べば確実に +3 になる
  • 前の値に依存して State を更新するときは必ず関数形式を使う習慣をつけること

解説10:複数コンポーネント間でStateを共有しよう

【解答例】

import { useState } from 'react';

const users = [
  { id: 1, name: "田中太郎", dept: "開発部"    },
  { id: 2, name: "鈴木花子", dept: "デザイン部" },
  { id: 3, name: "佐藤次郎", dept: "開発部"    },
  { id: 4, name: "山田三郎", dept: "営業部"    },
  { id: 5, name: "伊藤美咲", dept: "デザイン部" },
];

function FilterInput({ value, onChange }) {
  return (
    <input
      value={value}
      onChange={onChange}
      placeholder="名前・部署で検索..."
    />
  );
}

function UserList({ users }) {
  return (
    <ul>
      {users.map((u) => (
        <li key={u.id}>{u.name} — {u.dept}</li>
      ))}
    </ul>
  );
}

function App() {
  const [query, setQuery] = useState("");

  const filtered = users.filter((u) =>
    u.name.includes(query) || u.dept.includes(query)
  );

  return (
    <div>
      <FilterInput value={query} onChange={(e) => setQuery(e.target.value)} />
      <UserList users={filtered} />
    </div>
  );
}

【解説】

  • 「State のリフトアップ」は、複数コンポーネントで同じデータを扱うとき、共通の親に State を持たせる設計パターン
  • FilterInputquery State を持たず、valueonChange を受け取るだけの「制御されたコンポーネント」
  • フィルタリング処理は App 側で行い、結果だけを UserList に渡すことで責務が分離できる

解説11:バリデーション付きフォームを実装しよう

【解答例】

import { useState } from 'react';

function SignupForm() {
  const [form,   setForm]   = useState({ username: '', email: '', password: '' });
  const [errors, setErrors] = useState({});

  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm(prev => ({ ...prev, [name]: value }));
  };

  const validate = () => {
    const newErrors = {};
    if (form.username.length < 3)     newErrors.username = "ユーザー名は3文字以上で入力してください";
    if (!form.email.includes("@"))    newErrors.email    = "正しいメールアドレスを入力してください";
    if (form.password.length < 8)     newErrors.password = "パスワードは8文字以上で入力してください";
    return newErrors;
  };

  const handleSubmit = () => {
    const newErrors = validate();
    setErrors(newErrors);
    if (Object.keys(newErrors).length === 0) {
      alert("送信完了!");
      setForm({ username: '', email: '', password: '' });
      setErrors({});
    }
  };

  return (
    <div>
      <div>
        <input name="username" value={form.username} onChange={handleChange} placeholder="ユーザー名" />
        {errors.username && <p style={{ color: 'red' }}>{errors.username}</p>}
      </div>
      <div>
        <input name="email" value={form.email} onChange={handleChange} placeholder="メールアドレス" />
        {errors.email && <p style={{ color: 'red' }}>{errors.email}</p>}
      </div>
      <div>
        <input name="password" type="password" value={form.password} onChange={handleChange} placeholder="パスワード" />
        {errors.password && <p style={{ color: 'red' }}>{errors.password}</p>}
      </div>
      <button onClick={handleSubmit}>送信</button>
    </div>
  );
}

【解説】

  • バリデーション処理を validate() 関数に切り出すことで、handleSubmit の可読性が上がる
  • errors を State として管理することで、送信時に自動的にエラーメッセージが画面に反映される
  • Object.keys(newErrors).length === 0 はオブジェクトにキーが1つもない(=エラーなし)ことの確認方法として定番

解説12:TodoアプリでProps・Stateを総合的に使おう

【解答例】

import { useState } from 'react';

function TodoInput({ value, onChange, onAdd }) {
  return (
    <div>
      <input value={value} onChange={onChange} placeholder="タスクを入力..." />
      <button onClick={onAdd}>追加</button>
    </div>
  );
}

function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <li>
      <span
        onClick={onToggle}
        style={{ textDecoration: todo.done ? 'line-through' : 'none', cursor: 'pointer' }}
      >
        {todo.done ? "☑" : "☐"} {todo.text}
      </span>
      <button onClick={onDelete}>削除</button>
    </li>
  );
}

function TodoApp() {
  const [todos, setTodos] = useState([
    { id: 1, text: "牛乳を買う",       done: false },
    { id: 2, text: "Reactを練習する",  done: false },
    { id: 3, text: "メールを確認する", done: true  },
  ]);
  const [input, setInput] = useState("");

  const handleAdd = () => {
    if (input.trim() === "") return;
    setTodos(prev => [...prev, { id: Date.now(), text: input.trim(), done: false }]);
    setInput("");
  };

  const handleToggle = (id) => {
    setTodos(prev =>
      prev.map(t => t.id === id ? { ...t, done: !t.done } : t)
    );
  };

  const handleDelete = (id) => {
    setTodos(prev => prev.filter(t => t.id !== id));
  };

  const remaining = todos.filter(t => !t.done).length;

  return (
    <div>
      <TodoInput
        value={input}
        onChange={(e) => setInput(e.target.value)}
        onAdd={handleAdd}
      />
      <ul>
        {todos.map((todo) => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={() => handleToggle(todo.id)}
            onDelete={() => handleDelete(todo.id)}
          />
        ))}
      </ul>
      <p>残り{remaining}件 / 全{todos.length}件</p>
    </div>
  );
}

【解説】

  • すべての State(todosinput)は TodoApp(親)が管理し、子コンポーネントは Props を受け取るだけ
  • 完了切り替えは map() で「対象だけ done を反転、他はそのまま」の新しい配列を作る
  • remaining は State ではなく毎回 filter() で計算する。これが「導出可能な値は State に持たせない」の実践