第10回
イベントハンドリング:クリック・入力・キーボードを受け取る
ReactのonClick・onChange・onKeyDownなどのイベント処理を解説。アロー関数で包む理由、イベントオブジェクトの使い方、よくある間違いを実例で学ぶ。
·16分で読める
たける
ロスターのボタン、`onClick={setSelectedId(c.id)}` って書いたらページを開いた瞬間にクリックされた状態になっちゃって……。
りこ
`()` がついてるから即実行されてる。`onClick` には「関数」を渡す。「関数の呼び出し」じゃなくて。
onClickには「関数」を渡す
// ❌ これは「関数の呼び出し」──レンダリング時に即実行される
<button onClick={setSelectedId(c.id)}>
// ✅ これは「関数」──クリックされたときだけ実行される
<button onClick={() => setSelectedId(c.id)}>onClick={setSelectedId(c.id)} と書くと、JSXが評価される(=コンポーネントが描画される)瞬間に setSelectedId(c.id) が実行される。これがstateを更新するので再レンダリングが起き、またJSXが評価されて…と無限ループになる。
() => で包むことで「クリックされたときに実行する関数」になる。
たける
引数がない場合、たとえば `onClick={handleClick}` と書くときは `()=>` いらないですよね?
りこ
そう。関数をそのまま渡せる。`handleClick` はすでに関数だから。`() => handleClick()` と書いても動くけど、冗長になる。
イベントハンドラの書き方パターン
// パターン1:インラインで書く(シンプルな処理)
<button onClick={() => setSelectedId(c.id)}>
// パターン2:外に関数を切り出す(処理が複雑なとき)
const handleSelect = (id: string) => {
setSelectedId(id)
// ここに他の処理も書ける
}
<button onClick={() => handleSelect(c.id)}>
// パターン3:引数なし(関数をそのまま渡す)
const handleReset = () => setSelectedId('riko')
<button onClick={handleReset}>イベントオブジェクト
イベントハンドラにはイベントオブジェクトが渡される。クリックされた座標、押されたキー、入力値などの情報が入っている。
// クリックイベント
<button onClick={(e) => {
console.log(e.target) // クリックされた要素
e.preventDefault() // デフォルト動作をキャンセル
}}>
// 入力イベント──入力値は e.target.value で取れる
<input onChange={(e) => setName(e.target.value)} />
// キーボードイベント
<input onKeyDown={(e) => {
if (e.key === 'Enter') handleSubmit()
}} />検索入力を実装する
登場人物ページに名前で絞り込む検索ボックスを追加してみる。
import { useState } from 'react'
const CharactersPage = () => {
const [selectedId, setSelectedId] = useState(characters[0].id)
const [query, setQuery] = useState('') // 検索クエリのstate
// queryで絞り込んだキャラクター一覧
const filtered = characters.filter(c =>
c.name.includes(query) || c.role.includes(query)
)
const selected = characters.find(c => c.id === selectedId)!
return (
<div>
{/* 検索ボックス */}
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="名前・役職で検索"
/>
{/* ロスター(絞り込み済み) */}
<div>
{filtered.map(c => (
<RosterCard
key={c.id}
character={c}
isSelected={c.id === selectedId}
onClick={() => setSelectedId(c.id)}
/>
))}
{filtered.length === 0 && <p>該当するキャラクターがいません</p>}
</div>
{/* 詳細 */}
<CharacterCard character={selected} />
</div>
)
}
たける
入力するたびに `query` のstateが変わって、`filtered` が再計算されて、ロスターが更新されるってことですよね。リアルタイム検索が数行で書けた。
りこ
「stateに基づいてUIを計算して表示する」の典型例。jQueryで同じことをやると、入力のたびにDOMを手動で操作しないといけない。
よく使うイベントまとめ
| イベント | 使いどころ |
|---|---|
onClick |
ボタン・リンク・カードのクリック |
onChange |
input・select・textareaの入力変化 |
onSubmit |
フォームの送信(e.preventDefault() とセットで) |
onKeyDown |
キーボード入力(Enterキーで送信など) |
onFocus / onBlur |
入力欄のフォーカス・フォーカスアウト |
onMouseEnter / onMouseLeave |
ホバー状態の制御 |
📁 第10回完了時点のファイル構成・完成コード
フォルダ構成
src/
├── components/
│ ├── Header.tsx
│ ├── Footer.tsx
│ ├── CharacterCard.tsx
│ └── RosterCard.tsx ← 今回追加
├── pages/
│ └── CharactersPage.tsx ← 検索機能を追加
└── App.tsxRosterCard.tsx
type RosterCardProps = {
character: { id: string; name: string; role: string; icon?: string; accentColor: string }
isSelected: boolean
onClick: () => void
}
export const RosterCard = ({ character: c, isSelected, onClick }: RosterCardProps) => {
return (
<button
onClick={onClick}
className={`flex items-center gap-3 p-3 rounded-xl border-2 text-left w-full transition-all ${
isSelected
? 'border-sky-400 bg-sky-50'
: 'border-slate-100 bg-white hover:border-slate-300'
}`}
>
{c.icon ? (
<img src={c.icon} alt={c.name} className="w-10 h-10 rounded-full object-cover object-top" />
) : (
<div
className="w-10 h-10 rounded-full flex items-center justify-center text-white font-bold text-sm shrink-0"
style={{ backgroundColor: c.accentColor }}
>
{c.name[0]}
</div>
)}
<div>
<p className="text-sm font-semibold text-slate-900">{c.name}</p>
<p className="text-xs text-slate-500">{c.role}</p>
</div>
</button>
)
}CharactersPage.tsx(検索機能追加)
import { useState } from 'react'
import { CharacterCard } from '../components/CharacterCard'
import { RosterCard } from '../components/RosterCard'
const characters = [/* ...第9回と同じデータ */]
export const CharactersPage = () => {
const [selectedId, setSelectedId] = useState(characters[0].id)
const [query, setQuery] = useState('')
const filtered = characters.filter(c =>
c.name.includes(query) || c.role.includes(query)
)
const selected = characters.find(c => c.id === selectedId)!
return (
<div className="max-w-5xl mx-auto px-4 sm:px-8 py-12">
<h1 className="text-3xl font-bold text-slate-900 mb-8">登場人物</h1>
<div className="grid grid-cols-1 md:grid-cols-[280px_1fr] gap-8">
{/* 左ペイン:検索 + ロスター */}
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="名前・役職で検索"
className="w-full px-4 py-2 rounded-xl border border-slate-200 text-sm mb-4 outline-none focus:ring-2 focus:ring-sky-400"
/>
<div className="flex flex-col gap-2">
{filtered.map(c => (
<RosterCard
key={c.id}
character={c}
isSelected={c.id === selectedId}
onClick={() => setSelectedId(c.id)}
/>
))}
{filtered.length === 0 && (
<p className="text-sm text-slate-400 text-center py-4">
該当するキャラクターがいません
</p>
)}
</div>
</div>
{/* 右ペイン:詳細 */}
<CharacterCard character={selected} />
</div>
</div>
)
}まとめ
onClickには関数を渡す。onClick={fn()}は即実行になるのでonClick={() => fn()}と書く- 引数なしならそのまま渡せる:
onClick={handleClick} - 入力値は
onChange={(e) => setValue(e.target.value)}で取得 - stateと組み合わせるとリアルタイムな絞り込みが数行で書ける
次の第11回では、条件付きレンダリングを学ぶ。画像がまだないキャラクターのプレースホルダー表示を実装する。