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

2025-08-08

初級問題(9問)

イベント処理(onClickなど)

  1. ボタンクリック時に「Clicked!」とコンソールに表示するボタンコンポーネントを作成してください。
  2. 以下のコードの間違いを修正してください(イベントハンドラのthisバインディング問題):
class Button extends React.Component {
  handleClick() {
    console.log('Button clicked');
  }

  render() {
    return <button onClick={this.handleClick}>Click me</button>;
  }
}
  1. 関数コンポーネントでクリックイベントを処理し、クリック回数を表示するカウンターを作成してください。

useEffect基本

  1. コンポーネントマウント時に「Component mounted」とコンソールに表示するuseEffectを作成してください。
  2. 以下のuseEffectの依存配列の意味を説明し、空配列を渡した場合の挙動を説明してください:
useEffect(() => {
  console.log('Effect ran');
}, []);
  1. コンポーネントのアンマウント時に「Component unmounted」とコンソールに表示するuseEffectを作成してください。

フォーム基本(controlled components)

  1. テキスト入力フィールドを制御されたコンポーネント(controlled component)として実装してください。
  2. チェックボックスを制御されたコンポーネントとして実装してください。
  3. セレクトボックスで選択された値をstateで管理するコンポーネントを作成してください。

中級問題(15問)

イベント処理応用

  1. ボタンクリック時に親コンポーネントにイベントを伝播する子コンポーネントを作成してください。
  2. 入力フィールドでEnterキーが押されたときだけ処理を実行するイベントハンドラを作成してください。
  3. マウスオーバー時にツールチップを表示し、マウスアウト時に非表示にするコンポーネントを作成してください。
  4. イベント伝播を止めるために使用するメソッドを説明し、クリックイベントの伝播を止めるコードを書いてください。
  5. カスタムフックuseEventListenerを作成し、ウィンドウのresizeイベントを監視してください。

useEffect応用

  1. 特定のstate(例:userId)が変更された時だけAPIを呼び出すuseEffectを作成してください。
  2. クリーンアップ関数を使用して、setIntervalをクリーンアップするuseEffectを作成してください。
  3. 複数のuseEffectを使用して、マウント時、更新時、アンマウント時にそれぞれ異なる処理を実行するコンポーネントを作成してください。
  4. カスタムフックuseDocumentTitleを作成し、ドキュメントタイトルを動的に変更できるようにしてください。
  5. 以下の無限ループを修正してください。
// 無限ループの例

function InfiniteLoop() {

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

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

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

}

フォーム処理応用

  1. 複数の入力フィールドを単一のstateオブジェクトで管理するフォームを作成してください。
  2. フォーム送信時にバリデーションを実行し、エラーメッセージを表示するコンポーネントを作成してください。
  3. 入力値のデバウンス処理を実装した検索フォームを作成してください(入力後500ms経ってから検索を実行)。
  4. ファイルアップロード用のinputを制御されたコンポーネントとして実装してください。
  5. フォームリセット機能を実装してください(すべてのフィールドを初期値に戻す)。

上級問題(6問)

  1. コンポーネント外のクリックを検出するカスタムフックuseClickOutsideを作成してください。
  2. useReducerを使用して複雑なフォーム状態を管理するコンポーネントを作成してください。
  3. フォーム入力の履歴を保持し、undo/redo機能を実装してください。
  4. 動的にフォームフィールドを追加・削除できるフォームコンポーネントを作成してください。
  5. コンポーネントのマウント/アンマウント時にアニメーションを実行するuseEffectフックを作成してください。
  6. パフォーマンス最適化のために、イベントハンドラのメモ化が必要なケースを説明し、実装してください。

解答例

初級問題解答

  1. ボタンクリック時に「Clicked!」とコンソールに表示
function ClickButton() {
  const handleClick = () => {
    console.log('Clicked!');
  };
  return <button onClick={handleClick}>Click me</button>;
}
  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. クリック回数を表示するカウンター
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>
  );
}
  1. コンポーネントマウント時に「Component mounted」とコンソールに表示
function MyComponent() {
  useEffect(() => {
    console.log('Component mounted');
  }, []);

  return <div>My Component</div>;
}
  1. 空の依存配列を渡すと、useEffectはコンポーネントのマウント時に1度だけ実行され、その後は再実行されません。
  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>
  );
}