ReactでユーザーのインタラクションをTypeScriptで型安全に処理することは、堅牢なWebアプリケーションを構築する上で重要なスキルです。本記事では、クリック、入力、フォーム送信など、よく使用されるイベントハンドラの実装方法を詳しく解説します。
Reactのイベントシステムの基本
SyntheticEventとは
ReactではブラウザのネイティブイベントをラップしたSyntheticEvent(合成イベント)を使用します。これにより、ブラウザ間の差異を吸収し、一貫したAPIを提供します。
TypeScriptでは、イベントの型を明示的に指定することで、型安全性を確保できます:
// 基本的なイベントハンドラの型
const handleClick = (event: React.MouseEvent) => {
console.log('ボタンがクリックされました');
};
const handleChange = (event: React.ChangeEvent) => {
console.log('入力値が変更されました:', event.target.value);
};
よく使用されるイベント型一覧
Reactで頻繁に使用されるイベント型:
React.MouseEvent - クリック、マウス操作React.ChangeEvent - 入力値の変更React.FormEvent - フォーム送信React.KeyboardEvent - キーボード操作React.FocusEvent - フォーカス関連React.SubmitEvent - フォーム送信(HTML5)
ジェネリック型Tには、イベントが発生するHTML要素の型を指定します。
クリックイベントの実装
基本的なクリックハンドラ
最もシンプルなクリックイベントの実装:
import React, { useState } from 'react';
const ClickExample: React.FC = () => {
const [clickCount, setClickCount] = useState(0);
// ボタンクリックのハンドラ
const handleButtonClick = (event: React.MouseEvent) => {
console.log('クリックされた要素:', event.currentTarget);
console.log('クリック座標:', event.clientX, event.clientY);
setClickCount(prev => prev + 1);
};
// divクリックのハンドラ(イベントバブリング対策)
const handleDivClick = (event: React.MouseEvent) => {
// イベントの伝播を停止
event.stopPropagation();
console.log('Divがクリックされました');
};
return (
クリックイベントの例
クリック回数: {clickCount}
クリックしてください
異なる要素でのクリックイベント
様々なHTML要素でのクリックイベントの型指定:
const ClickVariations: React.FC = () => {
// リンクのクリック
const handleLinkClick = (event: React.MouseEvent) => {
event.preventDefault(); // ページ遷移を防ぐ
console.log('リンクがクリックされました');
};
// 画像のクリック
const handleImageClick = (event: React.MouseEvent) => {
console.log('画像がクリックされました:', event.currentTarget.src);
};
// リストアイテムのクリック
const handleListItemClick = (event: React.MouseEvent) => {
const itemText = event.currentTarget.textContent;
console.log('リストアイテムがクリックされました:', itemText);
};
return (
クリック可能なリンク
アイテム1
アイテム2
アイテム3
); };
入力イベントの処理
テキスト入力の処理
input要素での入力値の処理:
const InputExample: React.FC = () => {
const [inputValue, setInputValue] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// テキスト入力の処理
const handleTextChange = (event: React.ChangeEvent) => {
const value = event.target.value;
setInputValue(value);
// リアルタイムバリデーション
if (value.length > 10) {
console.log('入力が10文字を超えました');
}
};
// メールアドレスの処理
const handleEmailChange = (event: React.ChangeEvent) => {
const value = event.target.value;
setEmail(value);
// メールアドレスの形式チェック
const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
if (value && !isValidEmail) {
console.log('無効なメールアドレス形式です');
}
};
// パスワード入力の処理
const handlePasswordChange = (event: React.ChangeEvent) => {
const value = event.target.value;
setPassword(value);
// パスワード強度チェック
const hasMinLength = value.length >= 8;
const hasUppercase = /[A-Z]/.test(value);
const hasLowercase = /[a-z]/.test(value);
const hasNumber = /\d/.test(value);
console.log('パスワード強度:', {
hasMinLength, hasUppercase, hasLowercase, hasNumber
});
};
return (
入力イベントの例
テキスト入力: 文字数: {inputValue.length}
メールアドレス:
パスワード:
); };
セレクトボックスとテキストエリア
select要素とtextarea要素での入力処理:
const SelectAndTextareaExample: React.FC = () => {
const [selectedOption, setSelectedOption] = useState('');
const [textareaValue, setTextareaValue] = useState('');
// セレクトボックスの処理
const handleSelectChange = (event: React.ChangeEvent) => {
const value = event.target.value;
setSelectedOption(value);
console.log('選択された値:', value);
};
// テキストエリアの処理
const handleTextareaChange = (event: React.ChangeEvent) => {
const value = event.target.value;
setTextareaValue(value);
// 文字数制限
if (value.length > 500) {
console.log('500文字以内で入力してください');
return;
}
};
return (
カテゴリを選択: 選択してください テクノロジー デザイン ビジネス
コメント:
{textareaValue.length}/500文字
); };
フォームイベントの処理
フォーム送信の実装
form要素の送信イベントの処理:
interface FormData {
}
const FormExample: React.FC = () => {
const [formData, setFormData] = useState({
});
const [errors, setErrors] = useState>({});
const [isSubmitting, setIsSubmitting] = useState(false);
// フォーム送信の処理
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault(); // ページリロードを防ぐ
// バリデーション
const newErrors: Partial = {};
if (!formData.name.trim()) {
newErrors.name = '名前は必須です';
}
if (!formData.email.trim()) {
newErrors.email = 'メールアドレスは必須です';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = '有効なメールアドレスを入力してください';
}
if (formData.age キーボードイベントの処理
キー操作の検出
キーボードイベントの実装例:
const KeyboardExample: React.FC = () => {
const [inputValue, setInputValue] = useState('');
const [keyInfo, setKeyInfo] = useState('');
// キー押下の処理
const handleKeyDown = (event: React.KeyboardEvent) => {
const { key, code, ctrlKey, altKey, shiftKey } = event;
setKeyInfo(`Key: ${key}, Code: ${code}, Ctrl: ${ctrlKey}, Alt: ${altKey}, Shift: ${shiftKey}`);
// 特定のキーの処理
switch (key) {
case 'Enter':
console.log('Enterキーが押されました');
break;
case 'Escape':
setInputValue(''); // Escapeでクリア
break;
case 'Tab':
console.log('Tabキーが押されました');
break;
}
// ショートカットキーの処理
if (ctrlKey && key === 's') {
event.preventDefault(); // ブラウザの保存を防ぐ
console.log('Ctrl+S が押されました(保存処理)');
}
};
// Enter キーでの送信処理
const handleKeyPress = (event: React.KeyboardEvent) => {
if (event.key === 'Enter' && inputValue.trim()) {
console.log('Enter で送信:', inputValue);
setInputValue('');
}
};
return (
キーボードイベントの例
検索ボックスの実装
リアルタイム検索機能を持つ入力ボックス:
interface SearchResult {
}
const SearchBox: React.FC = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isSearching, setIsSearching] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(-1);
// 模擬検索データ
const searchData: SearchResult[] = [
{ id: 1, title: 'React', description: 'JavaScriptライブラリ' },
{ id: 2, title: 'TypeScript', description: 'JavaScript の型付け拡張' },
{ id: 3, title: 'Vue.js', description: 'プログレッシブフレームワーク' },
{ id: 4, title: 'Angular', description: 'フルスタックフレームワーク' }
];
// 検索処理
const performSearch = async (searchQuery: string) => {
if (!searchQuery.trim()) {
setResults([]);
return;
}
setIsSearching(true);
// 模擬API呼び出し
await new Promise(resolve => setTimeout(resolve, 300));
const filteredResults = searchData.filter(item =>
item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.description.toLowerCase().includes(searchQuery.toLowerCase())
);
setResults(filteredResults);
setIsSearching(false);
setSelectedIndex(-1);
};
// 入力値の変更
const handleInputChange = (event: React.ChangeEvent) => {
const value = event.target.value;
setQuery(value);
performSearch(value);
};
// キーボードナビゲーション
const handleKeyDown = (event: React.KeyboardEvent) => {
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
setSelectedIndex(prev =>
prev カスタムイベントハンドラの作成
再利用可能なイベントハンドラ
共通のイベント処理ロジックをカスタムフックとして抽出:
// カスタムフック: useInputHandler
interface UseInputHandlerOptions {
initialValue?: string;
validator?: (value: string) => string | null;
onChange?: (value: string) => void;
}
function useInputHandler(options: UseInputHandlerOptions = {}) {
const { initialValue = '', validator, onChange } = options;
const [value, setValue] = useState(initialValue);
const [error, setError] = useState(null);
const handleChange = (event: React.ChangeEvent) => {
const newValue = event.target.value;
setValue(newValue);
// バリデーション
if (validator) {
const validationError = validator(newValue);
setError(validationError);
}
// コールバック呼び出し
if (onChange) {
onChange(newValue);
}
};
const reset = () => {
setValue(initialValue);
setError(null);
};
return {
value,
error,
handleChange,
reset,
value,
}
};
}
// 使用例
const CustomHookExample: React.FC = () => {
// メールアドレス入力
const emailInput = useInputHandler({
if (!value) return '메일주소를 입력해주세요';
if (!/\S+@\S+\.\S+/.test(value)) return '올바른 메일주소 형식이 아닙니다';
return null;
},
});
// パスワード入力
const passwordInput = useInputHandler({
if (value.length パフォーマンスの最適化
イベントハンドラのメモ化
useCallbackを使用したイベントハンドラの最適化:
import React, { useState, useCallback, useMemo } from 'react';
interface ListItem {
}
const OptimizedList: React.FC = () => {
const [items, setItems] = useState([
{ id: 1, name: 'タスク1', completed: false },
{ id: 2, name: 'タスク2', completed: true },
{ id: 3, name: 'タスク3', completed: false }
]);
// アイテムの完了状態を切り替え(メモ化)
const handleToggleComplete = useCallback((id: number) => {
setItems(prevItems =>
prevItems.map(item =>
item.id === id ? { ...item, completed: !item.completed } : item
)
);
}, []);
// アイテムの削除(メモ化)
const handleDeleteItem = useCallback((id: number) => {
setItems(prevItems => prevItems.filter(item => item.id !== id));
}, []);
// フィルタリングされたアイテム(メモ化)
const completedItems = useMemo(() => {
return items.filter(item => item.completed);
}, [items]);
return (
最適化されたリスト
完了: {completedItems.length} / {items.length}
{items.map(item => ( ))}
); }; // メモ化されたリストアイテムコンポーネント interface ListItemProps { } const ListItemComponent = React.memo(({ item, onToggle, onDelete }) => { console.log(`Rendering item: ${item.name}`); // レンダリング確認用 return (
まとめ
TypeScriptでReactのイベントハンドラを実装することで、型安全で保守しやすいコードを書くことができます。適切な型指定により、開発時にエラーを早期発見し、IDE の支援機能を最大限活用できます。
重要なポイント:
SyntheticEventの型を正しく指定する
preventDefault()やstopPropagation()の適切な使用
フォームバリデーションとエラーハンドリング
パフォーマンス最適化のためのメモ化
カスタムフックによるロジックの再利用
実際の開発では、ユーザビリティを考慮したイベント処理を心がけ、アクセシビリティにも配慮した実装を行うことが重要です。