Part 3 · 画面に動きをつける
19 / 64 章
イベントハンドラ
onClick に渡すのは「関数」であって「実行結果」ではない
押されたら何かする。入力されたら何かする。 アプリを作るというのは、突き詰めればこの繰り返しです。
React では、それを onClick や onChange といった props で書きます。渡すのは関数で、呼ぶタイミングは React が決めます。
Part 0 でやった「関数を値として扱う」が、そのまま実物になります。
渡すのは関数、括弧は付けない
onClick には「押されたときに呼んでほしい関数」を渡します。
// ○ 関数そのものを渡す
<Button onClick={reset}>リセット</Button>
// ✕ その場で実行され、結果が渡ってしまう
<Button onClick={reset()}>リセット</Button>下の書き方をすると、描き直されるたびに実行されます。 最初に画面が出たときも、そのあと更新されるたびにも動きます。 ボタンを押す前に動いてしまうわけです。 Part 0 の「関数を値として扱う」 でやった「括弧は今すぐ実行しろの合図」がそのまま当てはまります。
引数を渡したいときは包む
「押されたら setSelected("さとう") を呼びたい」 という場合、括弧を付けたら実行されてしまいます。 そこで、それを呼ぶだけの関数を新しく作って渡します。
onClick={() => setSelected("さとう")}引数のある処理は関数で包んで渡している
まだ選んでいません
map の中でボタンを作り、それぞれに違う値を渡しています。 この形は実際のアプリで山ほど出てきます。
click-basic.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
const members = ["さとう", "すずき", "たかはし"];
export function ClickBasic() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
const [selected, setSelected] = useState("まだ選んでいません");
// 引数のいらない関数は、そのまま渡す
const reset = () => setSelected("まだ選んでいません");
return (
<div className="flex flex-col gap-4">
<p className="rounded-md border p-3">{selected}</p>
<div className="flex flex-wrap gap-2">
{members.map((name) => (
<Button
key={name}
size="sm"
variant="outline"
// 引数を渡したいときは、それを呼ぶ関数で包む
onClick={() => setSelected(`${name} を選びました`)}
>
{name}
</Button>
))}
<Button size="sm" onClick={reset}>
リセット
</Button>
</div>
</div>
);
}何が起きたかを受け取る
イベントが起きたときに呼ばれる関数をハンドラ(取り扱い係)と呼びます。 ハンドラは、呼ばれるときに何が起きたのかを説明するオブジェクト を受け取ります。慣習的に event や e と名付けます。
いちばんよく使うのは、入力欄の値を取り出す場面です。
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setText(event.target.value);
};event.target… 出来事が起きた要素そのもの(この場合は入力欄)event.target.value… いま入力されている文字
打つたびにハンドラが呼ばれている
まだ入力されていません
event-object.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Input } from "@/components/ui/input";
import { useState } from "react";
export function EventObject() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
const [text, setText] = useState("");
// ハンドラは、何が起きたかを説明するオブジェクトを受け取る
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// event.target が入力欄そのもの。その value が今の文字
setText(event.target.value);
};
return (
<div className="flex flex-col gap-3">
<Input placeholder="何か入力してみる" onChange={handleChange} aria-label="何か入力してみる" />
<p className="rounded-md border p-3">
{text === "" ? "まだ入力されていません" : `${text.length} 文字: ${text}`}
</p>
</div>
);
}よく使うイベント
<button onClick={...}> // 押された
<input onChange={...}> // 入力が変わった
<form onSubmit={...}> // 送信された
<input onFocus={...}> // 入力欄が選ばれた
<input onBlur={...}> // 入力欄から離れた
<div onMouseEnter={...}> // マウスが乗ったいずれも on + 出来事 という名前で、 キャメルケースで書きます。JSX の章でやったとおり、 JavaScript の命名にそろえてあるためです。
既定の動きを止める
フォームの送信ボタンを押すと、ブラウザは既定でページを再読み込みします。 React で作ったアプリではたいてい困るので、これを止めます。
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault(); // 再読み込みを止める
// ここで送信の処理を書く
};入力して Enter を押してみる
入力欄で Enter を押しても送信されます。ボタンだけに onClick を付けていると、 Enter で送信したときに何も起きません。送信を受け取るのはボタンではなく formだと覚えてください。
click-basic.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
const members = ["さとう", "すずき", "たかはし"];
export function ClickBasic() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
const [selected, setSelected] = useState("まだ選んでいません");
// 引数のいらない関数は、そのまま渡す
const reset = () => setSelected("まだ選んでいません");
return (
<div className="flex flex-col gap-4">
<p className="rounded-md border p-3">{selected}</p>
<div className="flex flex-wrap gap-2">
{members.map((name) => (
<Button
key={name}
size="sm"
variant="outline"
// 引数を渡したいときは、それを呼ぶ関数で包む
onClick={() => setSelected(`${name} を選びました`)}
>
{name}
</Button>
))}
<Button size="sm" onClick={reset}>
リセット
</Button>
</div>
</div>
);
}クリックは、親にも伝わる
押した場所だけで話が終わるとはかぎりません。イベントは、内側から外側へ順に伝わっていきます。
よくあるのが、こういう形です。行全体が押せて、その中に削除ボタンがある。
「消す」を押して、記録を見る
- まだ何も押していません
「消す」を押しただけなのに、行のほうも押されたことになっています。 消したいだけなのに、行を開く処理まで走ってしまう、 という不具合になります。
止めるには、stopPropagation を呼びます。 上のトグルを「する」に変えて、もう一度押してみてください。
<button
onClick={(event) => {
event.stopPropagation(); // 親に伝えない
remove(id);
}}
>submit-bubble.tsx
"use client";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from "react";
/** 送信を止めた版と、止めていない版 */
export function SubmitForm() {
const [log, setLog] = useState<string[]>([]);
const [text, setText] = useState("");
const handleSubmit = (event: React.FormEvent) => {
// これを書かないと、ブラウザがページを再読み込みしてしまう
event.preventDefault();
setLog((current) => [...current, `送信: ${text || "(空)"}`]);
};
return (
<RenderBox title="form の送信">
<form onSubmit={handleSubmit} className="flex flex-col gap-2">
<div className="flex gap-2">
<Input
value={text}
onChange={(event) => setText(event.target.value)}
placeholder="何か書いて Enter か「送信」"
aria-label="送信するテキスト"
/>
{/* type を書かないと submit になる。ここでは意図してそうしている */}
<Button size="sm" type="submit">
送信
</Button>
</div>
<ul className="flex flex-col gap-1 text-sm text-muted-foreground">
{log.length === 0 ? (
<li>まだ送信していません</li>
) : (
log.map((line, index) => <li key={index}>{line}</li>)
)}
</ul>
</form>
</RenderBox>
);
}
/** クリックが親まで伝わることを見せる */
export function Bubbling() {
const [log, setLog] = useState<string[]>([]);
const [stop, setStop] = useState(false);
const add = (who: string) => setLog((current) => [...current, who]);
return (
<div className="flex flex-col gap-3">
<Button size="sm" variant="outline" onClick={() => setStop(!stop)}>
伝わるのを止める: {stop ? "する" : "しない"}
</Button>
{/* 行全体が押せる。その中に削除ボタンがある、というよくある形 */}
<div
onClick={() => add("行が押された")}
className="cursor-pointer rounded-md border p-4"
>
<div className="flex items-center gap-3">
<span>牛乳を買う</span>
<Button
size="sm"
variant="ghost"
className="ml-auto"
onClick={(event) => {
if (stop) event.stopPropagation();
add("削除ボタンが押された");
}}
>
消す
</Button>
</div>
</div>
<RenderBox title="押された順">
<ul className="flex flex-col gap-1 text-sm text-muted-foreground">
{log.length === 0 ? (
<li>まだ何も押していません</li>
) : (
log.map((line, index) => <li key={index}>{line}</li>)
)}
</ul>
</RenderBox>
<Button size="sm" variant="ghost" onClick={() => setLog([])}>
記録を消す
</Button>
</div>
);
}理解できたか確かめる
確認クイズ
onClick={handleClick()} と書くとどうなる?
確認クイズ
入力欄の値を取り出すには?
確認クイズ
form の中の「戻る」ボタンを押すと、なぜかページが再読み込みされる。原因は?
click-basic.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
const members = ["さとう", "すずき", "たかはし"];
export function ClickBasic() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
const [selected, setSelected] = useState("まだ選んでいません");
// 引数のいらない関数は、そのまま渡す
const reset = () => setSelected("まだ選んでいません");
return (
<div className="flex flex-col gap-4">
<p className="rounded-md border p-3">{selected}</p>
<div className="flex flex-wrap gap-2">
{members.map((name) => (
<Button
key={name}
size="sm"
variant="outline"
// 引数を渡したいときは、それを呼ぶ関数で包む
onClick={() => setSelected(`${name} を選びました`)}
>
{name}
</Button>
))}
<Button size="sm" onClick={reset}>
リセット
</Button>
</div>
</div>
);
}この章のまとめ
onClickには関数を渡す。括弧を付けない- 引数が必要なときは
() => f(引数)で包む - ハンドラは event を受け取る。 入力値は
event.target.value - 名前は on + 出来事 のキャメルケース
- フォームの再読み込みは
event.preventDefault()で止める
click-basic.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
const members = ["さとう", "すずき", "たかはし"];
export function ClickBasic() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
const [selected, setSelected] = useState("まだ選んでいません");
// 引数のいらない関数は、そのまま渡す
const reset = () => setSelected("まだ選んでいません");
return (
<div className="flex flex-col gap-4">
<p className="rounded-md border p-3">{selected}</p>
<div className="flex flex-wrap gap-2">
{members.map((name) => (
<Button
key={name}
size="sm"
variant="outline"
// 引数を渡したいときは、それを呼ぶ関数で包む
onClick={() => setSelected(`${name} を選びました`)}
>
{name}
</Button>
))}
<Button size="sm" onClick={reset}>
リセット
</Button>
</div>
</div>
);
}