React演習問題(イベント・ライフサイクル・フォーム)

2025-08-08

初級問題(9問)

イベント処理(onClickなど)

問題1

以下の仕様を満たすReactコンポーネントを作成してください。

「Click me」と表示されたボタンを画面に表示すること。
ボタンがクリックされたとき、コンソールに “Clicked!” と出力すること。
クリック時の処理は、インラインではなく関数として定義し、その関数をイベントハンドラーとして設定すること。
イベントは onClick を使用すること。

【完成イメージ】

[ Click me ] ←クリックするとコンソールに "Clicked!" が表示される

問題2

以下のコードの間違いを修正してください(イベントハンドラのthisバインディング問題)。

class Button extends React.Component {
  handleClick() {
    console.log('Button clicked');
  }

  render() {
    return <button onClick={this.handleClick}>Click me</button>;
  }
}

問題3

関数コンポーネントでクリックイベントを処理し、クリック回数を表示するカウンターを作成してください。

  1. ボタン「Click me」を画面に表示すること
  2. ボタンをクリックするたびに、クリック回数をカウントすること
  3. 初期表示ではクリック回数は 0回 とすること
  4. クリック回数は useState を使って管理すること
  5. クリックされるたびに回数を 1ずつ増加 させること
  6. 「Clicked ○ times」という形式で回数を画面に表示すること
  7. クリック処理は関数として定義し、onClick に設定すること

【完成イメージ】

[ Click me ]
Clicked 0 times(クリック)Clicked 1 times
Clicked 2 times
...

useEffect基本

問題4

コンポーネントマウント時に「Component mounted」とコンソールに表示するuseEffectを作成してください。

  1. 「My Component」というテキストを画面に表示すること
  2. コンポーネントが初めて表示されたとき(マウント時)に1回だけ処理を実行すること
  3. 実行する処理は、コンソールに “Component mounted” と出力すること
  4. 副作用の処理には useEffect を使用すること
  5. 上記の処理が1回だけ実行されるように依存配列を適切に設定すること

【完成イメージ】

画面: My Component(初回表示時のみコンソール)
Component mounted

問題5

以下のuseEffectの依存配列の意味を説明し、空配列を渡した場合の挙動を説明してください。

useEffect(() => {
  console.log('Effect ran');
}, []);

問題6

コンポーネントのアンマウント時に「Component unmounted」とコンソールに表示するuseEffectを作成してください。

ー useEffectの「クリーンアップ関数」を使います。
ー 依存配列の指定方法に注意してください。

フォーム基本(controlled components)

問題7

テキスト入力フィールドを制御されたコンポーネント(controlled component)として実装してください。

ー e.target.value を使うと入力値を取得できます。
ー valueonChange をセットで使うのがポイントです。

【実行例】

[入力欄           ](下には何も表示されていない)

ユーザーが「Hello」と入力した場合

[Hello           ]Hello

問題8

チェックボックスを制御されたコンポーネントとして実装してください。以下の条件とします。

コンポーネント名は TextInput とする。
テキスト入力欄(input)を1つ表示する。
useState を使って入力値を管理する(初期値は空文字)。
入力欄の value にstateを設定する。
onChangeイベントで入力内容を取得し、stateを更新する。
入力欄の値は、常にstateと一致するようにする。

初期状態

[               ]

ユーザーが「A」と入力

[A              ]

ユーザーが「ABC」と入力

[ABC            ]

問題9

セレクトボックスで選択された値をstateで管理するコンポーネントを作成してください。

コンポーネント名は SelectBox とする
セレクトボックス(select)を1つ表示する
選択された値を useState で管理する(初期値は空文字)
value にstateを設定し、選択状態をReactで制御する
onChangeイベントで選択された値を取得し、stateを更新する
初期表示として「Select an option」を表示する
選択された値を、セレクトボックスの下に表示する

初期状態
[Select an option ▼]

(下には何も表示されない)
「Option 1」を選択
[Option 1 ▼]

option1
「Option 3」を選択
[Option 3 ▼]

option3

中級問題(15問)

イベント処理応用

問題10

ボタンクリック時に親コンポーネントにイベントを伝播する子コンポーネントを作成してください。

親コンポーネント Parent を作成する
子コンポーネント Child を作成する
Parent から Child に関数をpropsとして渡す
Child にはボタンを1つ表示する
ボタンをクリックしたとき、親コンポーネントの関数が実行されるようにする
実行時にコンソールに Child button clicked と表示されるようにする

実行例
初期表示
[Click me]
ボタンをクリック
(コンソール出力)

Child button clicked

問題11

入力フィールドでEnterキーが押されたときだけ処理を実行するイベントハンドラを作成してください。

コンポーネント名は EnterKeyHandler とする
テキスト入力欄(input)を1つ表示する
キーボード入力を検知するイベントを設定する
ユーザーが「Enterキー」を押したときのみ処理を実行する
Enterキーが押されたら、コンソールに Enter key pressed と表示する

実行例
初期状態
[テキスト入力欄]
通常のキー入力(例:aキー)
(コンソール出力なし)
Enterキーを押した場合
(コンソール出力)

Enter key pressed

問題12

マウスオーバー時にツールチップを表示し、マウスアウト時に非表示にするコンポーネントを作成してください。

コンポーネント名は Tooltip とする
ボタンを1つ表示する(表示テキストは「Hover me」)
ボタンにマウスを乗せたとき(ホバー時)にツールチップを表示する
マウスがボタンから離れたときにツールチップを非表示にする
ツールチップの表示状態は useState で管理する
ツールチップは「Tooltip content」というテキストを表示する

実行例
初期状態
[Hover me]

(ツールチップは表示されていない)

ボタンにマウスを乗せたとき
[Hover me]

Tooltip content
マウスを外したとき
[Hover me]

(ツールチップ非表示)

問題13

イベント伝播を止めるために使用するメソッドを説明し、クリックイベントの伝播を止めるコードを書いてください。

コンポーネント名は StopPropagation とする
親要素として div を作成し、クリックされたらコンソールに Div clicked と表示する
div の中にボタンを1つ配置する
ボタンをクリックしたときにコンソールに Button clicked, but event won’t bubble up と表示する
ただし、ボタンをクリックしても親の div のクリックイベントは発火しないようにする
そのためにイベントの伝播を停止する処理を使う

実行例
初期表示
[Click me]
ボタンをクリックした場合
(コンソール出力)

Button clicked, but event won’t bubble up

👉 親のログは出ない

divの余白部分をクリックした場合
(コンソール出力)

Div clicked

問題14

カスタムフックuseEventListenerを作成し、ウィンドウのresizeイベントを監視してください。

① カスタムフック useEventListener を作成する

引数として以下を受け取る
イベント名(eventName)
イベントハンドラー(handler)
コンポーネントのマウント時に window にイベントリスナーを登録する
コンポーネントのアンマウント時にイベントリスナーを解除する
useEffect を使用して管理する
eventName または handler が変わった場合は再登録されるようにする

② ResizeListener コンポーネントを作成する

画面の幅(width)と高さ(height)をstateで管理する
初期値は window.innerWidth と window.innerHeight を使用する
ウィンドウサイズが変更されたときにstateを更新する
現在のウィンドウサイズを画面に表示する

実行例
初期表示
Window size: 1200 x 800

(※環境により初期サイズは変わる)

ウィンドウをリサイズした場合
Window size: 900 x 600
さらにリサイズ
Window size: 500 x 300

問題15

特定のstate(例:userId)が変更された時だけAPIを呼び出すuseEffectを作成してください。

コンポーネント名は UserProfile とする
propsとして userId を受け取る
useState を使ってユーザー情報を管理する(初期値は null)
useEffect を使ってAPIからユーザー情報を取得する
APIのURLは /api/users/{userId} の形式を使う
データ取得後、レスポンスをJSONとしてstateに保存する
userId が変更された場合は再度APIを呼び出す
ユーザー情報が取得できるまでは「Loading…」と表示する
取得後はユーザーの名前(user.name)を表示する

実行例
初期表示(データ取得中)
Loading…
userId = 1 の場合(取得後)
Taro
userId = 2 に変更した場合
Hanako

問題16

クリーンアップ関数を使用して、setIntervalをクリーンアップするuseEffectを作成してください。

コンポーネント名は Timer とする
useState を使ってカウント(count)を管理する
初期値は 0 とする
コンポーネントが表示されたときにタイマーを開始する
1秒ごとにカウントを1ずつ増やす
setInterval を使用する
コンポーネントが削除されたときはタイマーを必ず停止する
現在のカウントを画面に表示する

実行例
初期表示
Count: 0
1秒後
Count: 1
3秒後
Count: 3
10秒後
Count: 10

問題17

複数のuseEffectを使用して、マウント時、更新時、アンマウント時にそれぞれ異なる処理を実行するコンポーネントを作成してください。

コンポーネント名は LifecycleDemo とする
useState を使って count を管理する(初期値は0)
画面に現在の count を表示する
ボタンを1つ作成し、クリックするたびに count を1増やす

① マウント時

コンポーネントが初めて表示されたときに
コンソールに Component did mount と表示する

② 更新時(count変更時)

count が変わるたびに
コンソールに Count updated: 現在のcount を表示する

③ アンマウント時

コンポーネントが画面から削除されるときに
コンソールに Component will unmount と表示する

実行例
初回表示
Count: 0

コンソール出力:

Component did mount
ボタンを1回クリック
Count: 1

コンソール出力:

Count updated: 1
さらにクリック(Count = 3)
Count: 3

コンソール出力:

Count updated: 3
コンポーネント削除時

コンソール出力:

Component will unmount

問題18

以下のカスタムフックuseDocumentTitleを作成し、ドキュメントタイトルを動的に変更できるようにしてください。

① カスタムフック useDocumentTitle を作成する

引数として title を受け取る
useEffect を使って、ブラウザのタブタイトル(document.title)を更新する
title が変更されるたびにタイトルを更新するようにする
依存配列に title を設定する

② PageTitle コンポーネントを作成する

useState を使って count を管理する(初期値は0)
画面に現在の count を表示する
ボタンをクリックするたびに count を1増やす
useDocumentTitle を使って、以下の形式でタブタイトルを更新する
Count: {count}

実行例
初期状態

画面表示:

Count: 0

タブタイトル:

Count: 0
ボタンを1回クリック

画面表示:

Count: 1

タブタイトル:

Count: 1
さらにクリック(Count = 5)

画面表示:

Count: 5

タブタイトル:

Count: 5

問題19

以下の無限ループを修正してください。

// 無限ループの例

function InfiniteLoop() {

  const [count, setCount] = useState(0);
  useEffect(() => {

    setCount(count + 1); // state更新が再レンダリングを引き起こし、useEffectが再実行される

  }); // 依存配列なし
  return <div>Count: {count}</div>;

}

フォーム処理応用

問題20

複数の入力フィールドを単一のstateオブジェクトで管理するフォームを作成してください。

コンポーネント名は MultiInputForm とする
フォームの入力値を1つのstateオブジェクトで管理する
stateには以下の3つのプロパティを持たせる
firstName
lastName
email
各input要素は name 属性を持つ
すべてのinputの変更を1つの handleChange 関数で処理する
e.target.name を使って更新対象のプロパティを判定する
入力値はすべてstateと同期する(Controlled Components)
各入力欄にはplaceholderを設定する

実行例
初期状態

[First Name] [Last Name] [Email]

入力途中(例:名前入力)

First Name: Taro
Last Name:
Email:

全て入力後

First Name: Taro
Last Name: Yamada
Email: taro@example.com

問題21

フォーム送信時にバリデーションを実行し、エラーメッセージを表示するコンポーネントを作成してください。

コンポーネント名は ValidatedForm とする
フォームの入力値を formData というstateオブジェクトで管理する
formData には以下の2つのプロパティを持つ
 ー email
 ー password
エラーメッセージを errors というstateで管理する
入力欄はControlled Componentsとして実装する
handleChange で入力値を更新する
フォーム送信時に handleSubmit を実行する
送信時には e.preventDefault() を使用してページ遷移を防ぐ

バリデーション要件:フォーム送信時に以下のチェックを行うこと

email
 ー 未入力の場合:「Email is required」
password
 ー 未入力の場合:「Password is required」
6文字未満の場合:「Password must be at least 6 characters」
 ー 送信条件
バリデーションがすべて通った場合のみ
 ー コンソールに Form submitted: formData を出力する

実行例
初期状態

[Email input]
[Password input]
[Submit button]

未入力で送信した場合

Email is required
Password is required

パスワードが短い場合

入力:

Email: test@example.com
Password: 123

表示:

Password must be at least 6 characters
正しい入力の場合

入力:

Email: test@example.com
Password: 123456

コンソール出力:

Form submitted: { email: “test@example.com”, password: “123456” }

問題22

入力値のデバウンス処理を実装した検索フォームを作成してください(入力後500ms経ってから検索を実行)。

コンポーネント名は DebouncedSearch とする
テキスト入力欄(input)を1つ表示する
入力値は query というstateで管理する
検索結果は results というstateで配列として管理する
入力内容に応じて検索処理を行うが、即時実行ではなく遅延実行とする
ユーザーが入力してから500ms後に検索処理を実行する
入力が続いている場合は、前の検索処理をキャンセルする
検索処理では以下を行う
コンソールに Searching for: 検索文字列 を出力する
仮の検索結果(文字列配列)を results にセットする

実行例
初期状態
[Search input]

(結果なし)

ユーザーが「a」と入力

Searching for: a
Result 1 for a
Result 2 for a

すぐに「ab」と入力した場合

👉 「a」の検索はキャンセルされる

Searching for: ab
Result 1 for ab
Result 2 for ab

「react」と入力した場合

Searching for: react
Result 1 for react
Result 2 for react

問題23

ファイルアップロード用のinputを制御されたコンポーネントとして実装してください。

コンポーネント名は FileUpload とする
ファイル選択用のinput(type=”file”)を表示する
選択されたファイルをstateで管理する
state名は file とし、初期値は null とする
ファイルが選択されたとき、e.target.files[0] を取得してstateに保存する
ファイルが選択されている場合のみ、以下を画面に表示する
ファイル名(file.name)
ファイルサイズ(file.size)

実行例
初期状態
[ファイル選択ボタン]

(何も表示されない)

ファイル「sample.png」を選択した場合

Selected file: sample.png
Size: 245000 bytes

ファイル「document.pdf」を選択した場合

Selected file: document.pdf
Size: 102400 bytes

問題24

フォームリセット機能を実装してください(すべてのフィールドを初期値に戻す)。

コンポーネント名は ResettableForm とする
フォームの入力値を formData というstateで管理する
初期値として以下のオブジェクトを使用する
name: 空文字
email: 空文字
入力欄は2つ用意する
名前(name)
メールアドレス(email)
どちらの入力もControlled Componentsとして実装する
handleChange で入力値を更新する
すべての入力は name属性 を使って共通処理する

リセット機能は以下の通りです。

  • 「Reset」ボタンを押すとフォームの内容を初期状態に戻す
  • 初期状態(initialState)に再セットすることでリセットを実現する
  • Resetボタンはフォーム送信ではなく通常ボタンとして扱う

実行例
初期状態

Name: [ ]
Email: [ ]
[Reset button]

入力後

Name: Taro
Email: taro@example.com

Resetボタンをクリックした場合

Name: [ ]
Email: [ ]

上級問題(6問)

問題25

コンポーネント外のクリックを検出するカスタムフックuseClickOutsideを作成してください。

① カスタムフック useClickOutside を作成する

引数として以下を受け取る
ref(DOM要素への参照)
callback(外側クリック時に実行する関数)
以下の条件で動作させる
ドキュメント全体の mousedown イベントを監視する
クリックされた要素が ref の内部でない場合のみ callback を実行する
コンポーネントのアンマウント時にはイベントリスナーを解除する

② ClickOutsideExample コンポーネントを作成する

メニューの開閉状態を isOpen で管理する
初期状態ではメニューは閉じている
「Open Menu」ボタンをクリックするとメニューが表示される
メニュー部分は ref を設定する
メニュー外をクリックするとメニューが閉じる
メニュー内をクリックした場合は閉じない

実行例
初期状態

[Open Menu button]
(メニューは非表示)

ボタンをクリック

[Open Menu button]

Menu content

メニュー内をクリック

Menu content
(何も起こらない)

メニュー外をクリック

[Open Menu button]
(メニューが閉じる)

問題26

useReducerを使用して複雑なフォーム状態を管理するコンポーネントを作成してください。

① 状態管理(useReducer)

useReducer を使ってフォーム状態を管理する
stateには以下を含める
 ー email(文字列)
 ー password(文字列)
 ー errors(オブジェクト)
reducerでは以下の3つのアクションを処理する

FIELD_CHANGE

入力フィールドの値を更新する
action.field に対応するプロパティを更新する

VALIDATE

バリデーションを実行する
emailが空ならエラー:「Email is required」
passwordが空なら:「Password is required」
passwordが6文字未満なら:「Password must be at least 6 characters」
結果を errors に保存する

RESET

stateを初期状態に戻す

② コンポーネント要件

コンポーネント名は FormWithReducer とする
email・passwordの2つの入力欄を持つ
どちらもControlled Componentsとして実装する
入力変更時は dispatch(FIELD_CHANGE) を使う
送信時は dispatch(VALIDATE) を実行する
エラーがある場合は画面に表示する
エラーがない場合はコンソールに送信データを出力する

実行例
初期状態

Email: [ ]
Password: [ ]
未入力で送信
Email is required
Password is required
不正なパスワード入力

入力:

Email: test@example.com
Password: 123

表示:

Password must be at least 6 characters
正しい入力

入力:

Email: test@example.com
Password: 123456

コンソール出力:

Form submitted: { email: “test@example.com”, password: “123456”, errors: {} }

問題27

フォーム入力の履歴を保持し、undo/redo機能を実装してください。

① 状態管理

コンポーネント名は FormWithHistory とする
以下の3つのstateを使用する
 ー formData
   フォームの現在の入力値を管理する
   以下の構造を持つ
   ー name
   ー email
 ー history
   過去の入力状態を配列で管理する
   初期値は { name: ”, email: ” } を1件持つ配列
 ー historyIndex
   現在参照している履歴の位置を管理する
   初期値は 0

② 入力処理

nameとemailの2つのinputを持つ
どちらもControlled Componentsとして実装する
入力変更時は以下を行う
 ー formDataを更新する
 ー 新しい状態をhistoryに追加する
 ー ただし現在のindex以降の履歴は削除する
 ー historyIndexを最新位置に更新する

③ Undo機能

「Undo」ボタンを押すと1つ前の状態に戻る
historyIndexを1つ減らす
対応する履歴のformDataを復元する
先頭の場合は無効化する

④ Redo機能

「Redo」ボタンを押すと1つ後の状態に進む
historyIndexを1つ増やす
対応する履歴のformDataを復元する
最新の場合は無効化する

⑤ 表示要件

現在の履歴位置を表示する
形式は以下の通り
 ー History index: X of Y

実行例
初期状態
Name: [ ]
Email: [ ]

Undo (disabled)
Redo (disabled)

History index: 0 of 0
入力1回目(Name入力)
Name: Taro
Email:

履歴:

History index: 1 of 1
入力2回目(Email入力)
Name: Taro
Email: taro@example.com

履歴:

History index: 2 of 2
Undo実行
Name: Taro
Email:

History index: 1 of 2
Redo実行
Name: Taro
Email: taro@example.com

History index: 2 of 2

問題28

動的にフォームフィールドを追加・削除できるフォームコンポーネントを作成してください。

① 状態管理

コンポーネント名は DynamicForm とする
fields というstateで入力フィールドの配列を管理する
各要素は以下の構造を持つ
 ー id(ユニークな識別子)
 ー value(入力値)
初期状態では1つの入力フィールドを持つ

② フィールド追加機能

「Add Field」ボタンを押すと新しい入力欄を追加する
新しいフィールドにはユニークなidを付与する
初期値は空文字とする

③ フィールド削除機能

各入力欄に「Remove」ボタンを表示する
クリックしたフィールドのみ削除する
idを使って対象を特定する

④ 入力変更処理

各inputはControlled Componentsとして実装する
入力変更時は handleChange を使用する
対象のidをもとに該当フィールドだけを更新する
他のフィールドはそのまま保持する

⑤ 表示要件

現在のfields配列をJSON形式で画面に表示する
フォーマットは整形表示(インデントあり)

実行例
初期状態

[ Input ] [Add Field] [  { “id”: 1, “value”: “” } ]

フィールド追加

[ Input ] [ Input ] [Add Field] [  { “id”: 1, “value”: “” },  { “id”: 1723456789, “value”: “” } ]

入力後

Field 1: Hello Field 2: React [  { “id”: 1, “value”: “Hello” },  { “id”: 1723456789, “value”: “React” } ]

フィールド削除

Field 2 removed [  { “id”: 1, “value”: “Hello” } ]

問題29

コンポーネントのマウント/アンマウント時にアニメーションを実行するuseEffectフックを作成してください。

① カスタムフック useMountAnimation を作成する

引数として duration(ミリ秒)を受け取る
 ー デフォルト値は 500ms とする
内部で以下の2つのstateを管理する
 ー isMounted(マウント済みかどうか)
 ー isAnimating(アニメーション中かどうか)

② 動作要件

コンポーネントがマウントされたときに以下を実行する
 ー isMounted を true にする
 ー isAnimating を true にする
指定された duration 後に
 ー isAnimating を false にする
コンポーネントがアンマウントされた場合は
 ー タイマーを必ず解除する

③ 戻り値

フックは以下のオブジェクトを返す
 ー isMounted
 ー isAnimating

④ コンポーネント AnimatedBox

useMountAnimation を使用する
isMounted が false の場合は何も表示しない(nullを返す)
isMounted が true の場合のみ要素を表示する
isAnimating が true の間はクラス名 fade-in を付与する
アニメーション終了後はクラスを外す

実行例
初回表示直後(0ms)
(何も表示されない可能性あり or 即表示)
表示直後(0〜500ms)
Animated Content
(fade-inクラスが付与されている状態)
500ms後
Animated Content
(fade-inクラスが外れた状態)

問題30

パフォーマンス最適化のために、イベントハンドラのメモ化が必要なケースを説明し、実装してください。

① 子コンポーネントの作成

コンポーネント名は ExpensiveComponent とする
React.memo を使用してメモ化する
propsとして onClick を受け取る
ボタンを表示し、クリック時に onClick を実行する
レンダリング時にコンソールへ以下を出力する
ExpensiveComponent rendered

② 親コンポーネントの作成

コンポーネント名は ParentComponent とする
count をstateで管理する(初期値0)
「Increment」ボタンを押すと count を1増やす

③ useCallbackの要件

handleClick を定義する
useCallback を使用して関数をメモ化する
依存配列は空配列とする
関数内容はコンソールに
Button clicked と出力する

④ レンダリング挙動

ParentComponentが再レンダリングされても
handleClickの参照は変わらないようにする
ExpensiveComponentは不要な再レンダリングをしないようにする

実行例
初期表示

Count: 0
ExpensiveComponent rendered

Incrementボタンをクリック

Count: 1
ExpensiveComponent rendered(※useCallbackなしの場合)

最適化後(useCallbackあり)

Count: 1
(ExpensiveComponentは再レンダリングされない)


解答例

初級問題解答

  1. ボタンクリック時に「Clicked!」とコンソールに表示です。
import React from 'react';

function ClickButton() {
  const handleClick = () => {
    console.log('Clicked!');
  };

  return <button onClick={handleClick}>Click me</button>;
}

export default ClickButton;
  1. イベントハンドラのthisバインディング問題です。
class Button extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    console.log('Button clicked');
  }

  render() {
    return <button onClick={this.handleClick}>Click me</button>;
  }
}

// またはアロー関数を使用
class Button extends React.Component {
  handleClick = () => {
    console.log('Button clicked');
  };

  render() {
    return <button onClick={this.handleClick}>Click me</button>;
  }
}
  1. クリック回数を表示するカウンターです。
import React, { useState } from 'react';

function ClickCounter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <button onClick={handleClick}>Click me</button>
      <p>Clicked {count} times</p>
    </div>
  );
}

export default ClickCounter;
  1. コンポーネントマウント時に「Component mounted」とコンソールに表示します。
import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    console.log('Component mounted');
  }, []);

  return <div>My Component</div>;
}

export default MyComponent;
  1. 空の依存配列を渡すと、useEffectはコンポーネントのマウント時に1度だけ実行され、その後は再実行されません。解説は以下の通りです。

useEffectは、コンポーネントのレンダリング後に特定の処理(副作用)を実行するために使います。ここでいう副作用とは、例えばAPI通信、ログ出力、イベント登録など、画面の描画そのものとは直接関係しない処理を指します。このコードでは、コンポーネントがレンダリングされた後に「Effect ran」という文字列をコンソールに出力する処理が書かれています。次に、useEffectの第二引数である「依存配列」について。依存配列とは、そのuseEffectが「どの値の変化をきっかけに再実行されるか」を指定するものです。配列の中に変数を入れると、それらの値が変わったときにのみuseEffectが再実行されます。依存配列がどのような値かによって、useEffectの実行タイミングは大きく変わります。依存配列を省略した場合は、コンポーネントが再レンダリングされるたびに毎回実行されます。依存配列に特定の値を入れた場合は、その値が変化したときにのみ実行されます。そして今回のように空配列を渡した場合は、コンポーネントが最初に画面に表示されたとき(初回レンダリング時)にだけ一度実行され、その後は再レンダリングが起きても再度実行されることはありません。つまり、このコードは「コンポーネントが最初に表示されたときに一度だけログを出力する」動作になります。この挙動は、クラスコンポーネントでいうcomponentDidMountに近い動きと考えると理解しやすいです。

  1. コンポーネントのアンマウント時に「Component unmounted」とコンソールに表示します。
function MyComponent() {
  useEffect(() => {
    return () => {
      console.log('Component unmounted');
    };
  }, []);

  return <div>My Component</div>;
}
  1. テキスト入力フィールドを制御されたコンポーネントです。
function TextInput() {
  const [value, setValue] = useState('');
  const handleChange = (e) => {
    setValue(e.target.value);
  };
  return (
    <input type="text" value={value} onChange={handleChange} />
  );
}
  1. チェックボックスを制御されたコンポーネント
function Checkbox() {
  const [checked, setChecked] = useState(false);

  const handleChange = (e) => {
    setChecked(e.target.checked);
  };

  return (
    <input 
      type="checkbox" 
      checked={checked} 
      onChange={handleChange} 
    />
  );
}
  1. セレクトボックスで選択された値をstateで管理するコンポーネント
function SelectBox() {
  const [selectedValue, setSelectedValue] = useState('');
  const handleChange = (e) => {
    setSelectedValue(e.target.value);
  };
  return (
    <select value={selectedValue} onChange={handleChange}>
      <option value="">Select an option</option>
      <option value="option1">Option 1</option>
      <option value="option2">Option 2</option>
      <option value="option3">Option 3</option>
    </select>
  );
}

中級問題解答

  1. ボタンクリック時に親コンポーネントにイベントを伝播する子コンポーネント
// 親コンポーネント
function Parent() {
  const handleChildClick = () => {
    console.log('Child button clicked');
  };
  return <Child onClick={handleChildClick} />;
}

// 子コンポーネント
function Child({ onClick }) {
  return <button onClick={onClick}>Click me</button>;
}
  1. 入力フィールドでEnterキーが押されたときだけ処理を実行するイベントハンドラ
function EnterKeyHandler() {
  const handleKeyDown = (e) => {
    if (e.key === 'Enter') {
      console.log('Enter key pressed');
    }
  };

  return ;
}
  1. マウスオーバー時にツールチップを表示し、マウスアウト時に非表示にするコンポーネント
function Tooltip() {
  const [isVisible, setIsVisible] = useState(false);
  return (
    <div>
      <button
        onMouseOver={() => setIsVisible(true)}
        onMouseOut={() => setIsVisible(false)}
      >
        Hover me
      </button>
      {isVisible && <div className="tooltip">Tooltip content</div>}
    </div>
  );
}
  1. イベント伝播を止めるために使用するメソッドを説明し、クリックイベントの伝播を止めるコード

e.stopPropagation()を使用します。

function StopPropagation() {
  const handleClick = (e) => {
    e.stopPropagation();
    console.log('Button clicked, but event won't bubble up');
  };

  return (
    <div onClick={() => console.log('Div clicked')}>
      <button onClick={handleClick}>Click me</button>
    </div>
  );
}
  1. カスタムフックuseEventListenerを作成し、ウィンドウのresizeイベントを監視
function useEventListener(eventName, handler) {
  useEffect(() => {
    window.addEventListener(eventName, handler);
    return () => {
      window.removeEventListener(eventName, handler);
    };
  }, [eventName, handler]);
}

// 使用例
function ResizeListener() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEventListener('resize', () => {
    setWindowSize({
      width: window.innerWidth,
      height: window.innerHeight
    });
  });

  return (
    <div>
      Window size: {windowSize.width} x {windowSize.height}
    </div>
  );
}
  1. 特定のstate(例:userId)が変更された時だけAPIを呼び出すuseEffect
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(response => response.json())
      .then(data => setUser(data));
  }, [userId]);

  return <div>{user ? user.name : 'Loading...'}</div>;
}
  1. クリーンアップ関数を使用して、setIntervalをクリーンアップするuseEffect
function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  return <div>Count: {count}</div>;
}
  1. 複数のuseEffectを使用して、マウント時、更新時、アンマウント時にそれぞれ異なる処理
function LifecycleDemo() {
  const [count, setCount] = useState(0);

  // マウント時のみ
  useEffect(() => {
    console.log('Component did mount');
  }, []);

  // 更新時(countが変更された時)
  useEffect(() => {
    console.log('Count updated:', count);
  }, [count]);

  // アンマウント時
  useEffect(() => {
    return () => {
      console.log('Component will unmount');
    };
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
  1. カスタムフックuseDocumentTitleを作成し、ドキュメントタイトルを動的に変更
function useDocumentTitle(title) {
  useEffect(() => {
    document.title = title;
  }, [title]);
}

// 使用例
function PageTitle() {
  const [count, setCount] = useState(0);
  useDocumentTitle(`Count: ${count}`);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
  1. 無限ループを引き起こすuseEffectの修正
function FixedLoop() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    if (count < 10) {
      setCount(count + 1);
    }
  }, [count]); // countを依存配列に追加

  return <div>Count: {count}</div>;
}
  1. 複数の入力フィールドを単一のstateオブジェクトで管理するフォーム
function MultiInputForm() {
  const [formData, setFormData] = useState({ firstName: '', lastName: '', email: '' });

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

  return (
    <form>
      <input name="firstName" value={formData.firstName} onChange={handleChange} placeholder="First Name" />
      <input name="lastName" value={formData.lastName} onChange={handleChange} placeholder="Last Name" />
      <input name="email" value={formData.email} onChange={handleChange} placeholder="Email" />
    </form>
  );
}
  1. フォーム送信時にバリデーションを実行し、エラーメッセージを表示するコンポーネント
function ValidatedForm() {
  const [formData, setFormData] = useState({ email: '', password: '' });
  const [errors, setErrors] = useState({});

  const validate = () => {
    const newErrors = {};
    if (!formData.email) newErrors.email = 'Email is required';
    if (!formData.password) newErrors.password = 'Password is required';
    else if (formData.password.length < 6) newErrors.password = 'Password must be at least 6 characters';

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (validate()) {
      console.log('Form submitted:', formData);
    }
  };

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

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          name="email"
          value={formData.email}
          onChange={handleChange}
          placeholder="Email"
        />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>
      <div>
        <input
          name="password"
          type="password"
          value={formData.password}
          onChange={handleChange}
          placeholder="Password"
        />
        {errors.password && <span className="error">{errors.password}</span>}
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}
  1. 入力値のデバウンス処理を実装した検索フォーム
function DebouncedSearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  useEffect(() => {
    const timer = setTimeout(() => {
      if (query) {
        // 実際にはAPI呼び出しなどを行う
        console.log('Searching for:', query);
        setResults([`Result 1 for ${query}`, `Result 2 for ${query}`]);
      }
    }, 500);
    return () => clearTimeout(timer);
  }, [query]);

  return (
    <div>
      <input type="text" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search..." />
      <ul>
        {results.map((result, index) => (
          <li key={index}>{result}</li>
        ))}
      </ul>
    </div>
  );
}
  1. ファイルアップロード用のinputを制御されたコンポーネント
function FileUpload() {
  const [file, setFile] = useState(null);

  const handleChange = (e) => {
    setFile(e.target.files[0]);
  };

  return (
    <div>
      <input type="file" onChange={handleChange} />
      {file && (
        <div>
          <p>Selected file: {file.name}</p>
          <p>Size: {file.size} bytes</p>
        </div>
      )}
    </div>
  );
}
  1. フォームリセット機能
function ResettableForm() {
  const initialState = { name: '', email: '' };
  const [formData, setFormData] = useState(initialState);

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

  const handleReset = () => {
    setFormData(initialState);
  };

  return (
    <form>
      <input name="name" value={formData.name} onChange={handleChange} placeholder="Name" />
      <input name="email" value={formData.email} onChange={handleChange} placeholder="Email" />
      <button type="button" onClick={handleReset}>
        Reset
      </button>
    </form>
  );
}

上級問題解答

  1. コンポーネント外のクリックを検出するカスタムフックuseClickOutside
function useClickOutside(ref, callback) {
  useEffect(() => {
    const handleClickOutside = (event) => {
      if (ref.current && !ref.current.contains(event.target)) {
        callback();
      }
    };
    document.addEventListener('mousedown', handleClickOutside);
    return () => {
      document.removeEventListener('mousedown', handleClickOutside);
    };
  }, [ref, callback]);
}

// 使用例
function ClickOutsideExample() {
  const ref = useRef(null);
  const [isOpen, setIsOpen] = useState(false);

  useClickOutside(ref, () => setIsOpen(false));

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Open Menu</button>
      {isOpen && (
        <div ref={ref} className="menu">
          Menu content
        </div>
      )}
    </div>
  );
}
  1. useReducerを使用して複雑なフォーム状態を管理するコンポーネント

const formReducer = (state, action) => {
  switch (action.type) {
    case 'FIELD_CHANGE':
      return {
        ...state,
        [action.field]: action.value,
      };
    case 'VALIDATE':
      return {
        ...state,
        errors: {
          email: !state.email ? 'Email is required' : '',
          password: !state.password
            ? 'Password is required'
            : state.password.length < 6
              ? 'Password must be at least 6 characters'
              : '',
        },
      };
    case 'RESET':
      return initialState;
    default:
      return state;
  }
};

const initialState = {
  email: '',
  password: '',
  errors: {},
};

function FormWithReducer() {
  const [state, dispatch] = useReducer(formReducer, initialState);

  const handleChange = (e) => {
    dispatch({
      type: 'FIELD_CHANGE',
      field: e.target.name,
      value: e.target.value,
    });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    dispatch({ type: 'VALIDATE' });

    if (!state.errors.email && !state.errors.password) {
      console.log('Form submitted:', state);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          name="email"
          value={state.email}
          onChange={handleChange}
          placeholder="Email"
        />
        {state.errors.email && <span className="error">{state.errors.email}</span>}
      </div>
      <div>
        <input
          name="password"
          type="password"
          value={state.password}
          onChange={handleChange}
          placeholder="Password"
        />
        {state.errors.password && <span className="error">{state.errors.password}</span>}
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}
  1. フォーム入力の履歴を保持し、undo/redo機能
function FormWithHistory() {
  const [formData, setFormData] = useState({ name: '', email: '' });
  const [history, setHistory] = useState([{ name: '', email: '' }]);
  const [historyIndex, setHistoryIndex] = useState(0);

  const handleChange = (e) => {
    const { name, value } = e.target;
    const newFormData = { ...formData, [name]: value };
    setFormData(newFormData);
    // 新しい履歴を追加(現在のインデックス以降の履歴は削除)
    const newHistory = [...history.slice(0, historyIndex + 1), newFormData];
    setHistory(newHistory);
    setHistoryIndex(newHistory.length - 1);
  };

  const undo = () => {
    if (historyIndex > 0) {
      setHistoryIndex(historyIndex - 1);
      setFormData(history[historyIndex - 1]);
    }
  };

  const redo = () => {
    if (historyIndex < history.length - 1) {
      setHistoryIndex(historyIndex + 1);
      setFormData(history[historyIndex + 1]);
    }
  };

  return (
    <div>
      <form>
        <input name="name" value={formData.name} onChange={handleChange} placeholder="Name" />
        <input name="email" value={formData.email} onChange={handleChange} placeholder="Email" />
      </form>
      <div>
        <button onClick={undo} disabled={historyIndex === 0}>
          Undo
        </button>
        <button onClick={redo} disabled={historyIndex === history.length - 1}>
          Redo
        </button>
      </div>
      <div>
        History index: {historyIndex} of {history.length - 1}
      </div>
    </div>
  );
}
  1. 動的にフォームフィールドを追加・削除できるフォームコンポーネント
function DynamicForm() {
  const [fields, setFields] = useState([{ id: 1, value: '' }]);

  const addField = () => {
    setFields([...fields, { id: Date.now(), value: '' }]);
  };

  const removeField = (id) => {
    setFields(fields.filter(field => field.id !== id));
  };

  const handleChange = (id, value) => {
    setFields(fields.map(field =>
      field.id === id ? { ...field, value } : field
    ));
  };

  return (
    <div>
      <button onClick={addField}>Add Field</button>
      {fields.map(field => (
        <div key={field.id}>
          <input
            value={field.value}
            onChange={(e) => handleChange(field.id, e.target.value)}
          />
          <button onClick={() => removeField(field.id)}>Remove</button>
        </div>
      ))}
      <pre>{JSON.stringify(fields, null, 2)}</pre>
    </div>
  );
}
  1. コンポーネントのマウント/アンマウント時にアニメーションを実行するuseEffectフック
function useMountAnimation(duration = 500) {
  const [isMounted, setIsMounted] = useState(false);
  const [isAnimating, setIsAnimating] = useState(false);

  useEffect(() => {
    setIsMounted(true);
    setIsAnimating(true);
    const timer = setTimeout(() => setIsAnimating(false), duration);
    return () => clearTimeout(timer);
  }, [duration]);

  return { isMounted, isAnimating };
}

// 使用例
function AnimatedBox() {
  const { isMounted, isAnimating } = useMountAnimation();

  if (!isMounted) return null;

  return (
    <div className={`box ${isAnimating ? 'fade-in' : ''}`}>
      Animated Content
    </div>
  );
}
  1. パフォーマンス最適化のために、イベントハンドラのメモ化が必要なケース

// この例では、Incrementボタンをクリックするとcountが更新され、ParentComponentが再レンダリングされます。
// handleClickがuseCallbackでメモ化されていない場合、ExpensiveComponentも再レンダリングされます。
// useCallbackを使用すると、依存配列が変更されない限り同じ関数参照が保持されるため、
// ExpensiveComponentは不必要な再レンダリングを防ぐことができます。

// メモ化が必要なケース: 子コンポーネントにイベントハンドラを渡す場合など
const ExpensiveComponent = React.memo(function({ onClick }) {
  console.log('ExpensiveComponent rendered');
  return <button onClick={onClick}>Click me</button>;
});

function ParentComponent() {
  const [count, setCount] = useState(0);

  // メモ化されていないハンドラ - 親が再レンダリングされるたびに新しい関数が作成される
  // const handleClick = () => {
  //   console.log('Button clicked');
  // };

  // メモ化されたハンドラ
  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ExpensiveComponent onClick={handleClick} />
    </div>
  );
}