この連載記事は、これから React を学びたい JavaScript 開発者のための入門コンテンツです。対象とする React のバージョンは執筆時点で最新の v16.13 です。連載記事は以下の通り。
Reactとは何か
JSX
属性と状態
フォームとイベントハンドリング
ToDoアプリを作ってみよう
副作用
カスタムフック
Reactプロジェクトを始める方法
理解を助けることを意図としているため、網羅的、リファレンス的な解説はしていません。ドキュメントを併読すると、さらに理解が深まると思います。
本章では、React におけるイベント処理について説明します。と言っても、新しい概念や機能は登場しません。前章で学んだ props と state を応用してイベント処理を実装します。
フォーム
まずは、ネイティブ(既存の HTML 要素に備わっている)イベント処理の代表として、フォームの扱いについて学びましょう。
テキスト
フォーム入力は典型的に、入力値を保管する state と、onChange
で state を更新するイベントハンドラを組み合わせて実装します。
function App() {
const [name, setName] = React.useState('John');
const handleChange = e => setName(e.target.value);
return (
<>
<h1>Hello, {name}</h1>
<input value={name} onChange={handleChange} />
</>
);
}
const root = document.getElementById('root');
ReactDOM.render(<App />, root);
出力結果を確認しましょう。入力値が更新されると、表示も同時に更新されます。
See the Pen React Tutorial Example 4.1 by Masahiro Harada (@MasahiroHarada) on CodePen.
textarea
も input
と同様です。HTML 要素とは記述が異なるので注意しましょう。
<textarea value={val} onChange={handleChange} />
React は機能が絞られたライブラリです。イベント処理やフォーム処理のための特別な構文や機能は用意されていません。onChange
に渡されるハンドラの第一引数も、普通の Event
オブジェクトのラッパなので、たとえば input
の type
が file
の場合は、e.target.files
でアップロードされたファイルを取得することになります。
ラジオボタン
ラジオボタンの場合もほぼ同様ですが、checked
の値を明示的に判定する必要があります。
function App() {
const [val, setVal] = React.useState('cat');
const handleChange = e => setVal(e.target.value);
return (
<>
<label>
<input
type="radio"
value="cat"
onChange={handleChange}
checked={val === 'cat'}
/>
猫派
</label>
<label>
<input
type="radio"
value="dog"
onChange={handleChange}
checked={val === 'dog'}
/>
犬派
</label>
<p>選択値:{val}</p>
</>
);
}
See the Pen React Tutorial Example 4.2 by Masahiro Harada (@MasahiroHarada) on CodePen.
チェックボックス
チェックボックスもラジオボタンと同様です。ただし、複数選択になるので、入力値は配列で保持するのがよいでしょう。
function App() {
const [val, setVal] = React.useState(['js']);
const handleChange = e => {
// change したのはいいとして、ON なのか OFF なのか判定する必要がある
if (val.includes(e.target.value)) {
// すでに含まれていれば OFF したと判断し、
// イベント発行元を除いた配列を set し直す
setVal(val.filter(item => item !== e.target.value));
} else {
// そうでなければ ON と判断し、
// イベント発行元を末尾に加えた配列を set し直す
setVal([...val, e.target.value]);
// state は直接は編集できない
// つまり val.push(e.target.value) はNG ❌
}
};
return (
<>
<label>
<input
type="checkbox"
value="js"
onChange={handleChange}
checked={val.includes('js')}
/>
JavaScript
</label>
<label>
<input
type="checkbox"
value="python"
onChange={handleChange}
checked={val.includes('python')}
/>
Python
</label>
<label>
<input
type="checkbox"
value="java"
onChange={handleChange}
checked={val.includes('java')}
/>
Java
</label>
<p>選択値:{val.join(', ')}</p>
</>
);
}
See the Pen React Tutorial Example 4.3 by Masahiro Harada (@MasahiroHarada) on CodePen.
繰り返しになりますが、React はフォーム処理について特定の実装方法を提供しません。そのため、入力値をどのようなデータ構造で保持するかなどは、アプリの必要性に合わせて実装します。たとえば、チェックボックスは以下の方法でも実装できます。
function App() {
const initialVal = { js: true, python: false, java: false };
const [val, setVal] = React.useState(initialVal);
const handleChange = e => {
const newVal = Object.assign({}, val, {
[e.target.value]: !val[e.target.value]
});
setVal(newVal);
};
return (
<>
<label>
<input
type="checkbox"
value="js"
onChange={handleChange}
checked={val['js']}
/>
JavaScript
</label>
<label>
<input
type="checkbox"
value="python"
onChange={handleChange}
checked={val['python']}
/>
Python
</label>
<label>
<input
type="checkbox"
value="java"
onChange={handleChange}
checked={val['java']}
/>
Java
</label>
<p>選択値:{Object.keys(val).filter(item => val[item]).join(', ')}</p>
</>
);
}
セレクトボックス
セレクトボックスの場合は、<select>
の value
に入力値を指定します。
function App() {
const [val, setVal] = React.useState('react');
const handleChange = e => setVal(e.target.value);
return (
<>
<select value={val} onChange={handleChange}>
<option value="react">React</option>
<option value="vue">Vue.js</option>
<option value="angular">Angular</option>
</select>
<p>選択値:{val}</p>
</>
);
}
See the Pen React Tutorial Example 4.4 by Masahiro Harada (@MasahiroHarada) on CodePen.
コンポーネントのイベント処理
続いて、自作コンポーネントにおけるイベント処理について学びましょう。
props で関数を渡す実装方法はネイティブイベントと同様です。ただし、自作コンポーネントでイベントパターンを用いるには、まず「いつどこで使うか」を判断できなくてはいけません。この点に関しては、コンポーネントの親子関係の文脈で理解するとよいでしょう。
「属性と状態」の章で、子コンポーネントは親コンポーネントから渡された props を直接更新できないと説明しました。
function Parent() {
const [val, setVal] = React.useState(0);
return <Child value={val} />;
}
function Child({ value }) {
value = 123; // ❌ こういうことはできない
// ...
}
しかし、親コンポーネントで管理する state を、子コンポーネント内のボタンが押されたときや、非同期処理が終わったときなど、子コンポーネントでしか分からないタイミングで、操作したい場面があると思います。そういうときに、イベントパターンが役立ちます。
function Parent() {
const [val, setVal] = React.useState(0);
// 子に渡すイベントハンドラ
// ここで、子で指定される引数(例では value)をもとに state を更新する
const handleUpdate = value => setVal(value);
return <Child onUpdate={handleUpdate} />;
}
function Child({ onUpdate }) {
onUpdate(123); // ✅ 親から渡された関数を実行する
// ...
}
以下は、タブ機能を実装したサンプルです。
function Tab({ onChange }) {
return (
<ul>
<li onClick={() => onChange(1)}>React</li>
<li onClick={() => onChange(2)}>Vue.js</li>
<li onClick={() => onChange(3)}>Anguar</li>
</ul>
);
}
function App() {
const [tab, setTab] = React.useState(1);
const handleChange = val => setTab(val);
return (
<>
<Tab onChange={handleChange} />
<div hidden={tab !== 1}>
A JavaScript library for building user interfaces
</div>
<div hidden={tab !== 2}>
The Progressive JavaScript Framework
</div>
<div hidden={tab !== 3}>
One framework. Mobile & desktop.
</div>
</>
);
}
const root = document.getElementById('root');
ReactDOM.render(<App />, root);
要点が伝わりやすいようにスタイルは省きました。
See the Pen React Tutorial Example 4.5 by Masahiro Harada (@MasahiroHarada) on CodePen.
子コンポーネントである Tab
は、直接親コンポーネントの state を更新せず、props として渡された関数を実行します。その関数が何をするかは、親が定義します。引数として、子しか知らない情報を受け取ることもできます。
このようなイベントパターンは、親子コンポーネント間のコミュニケーション方法と捉えることもできるでしょう。
練習問題
練習問題に取り組みましょう。自分で考えてみることが大切ですが、もし解けなくても、解答を見て写し書きするだけでも理解が深まると思います。
問題 1
以下のフォームを作成しましょう。
「その他」を選ぶと、自由記入欄が表示されます。
/* ??? */
を埋めて、コードを完成させてください。
(練習のため、コピペではなく写して書きながら埋めるとよいです。)
const options = [
{ value: 'js', label: 'JavaScript' },
{ value: 'py', label: 'Python' },
{ value: 'rb', label: 'Ruby' },
{ value: '', label: 'その他' },
];
function App() {
/* ??? */
const getAnswer = () => {
/* ??? */
// その他が選択されている場合は、自由記入欄の入力値を返す
// それ以外の場合は、options 配列の該当する要素の label を返す
};
return (/* ??? */);
}
const root = document.getElementById('root');
ReactDOM.render(<App />, root);
ヒント
- ラジオボタンは、
options
を元にループで生成しましょう。 - ラジオボタンとテキストそれぞれを管理する state を作成します。
- 回答の表示は、
getAnswer
関数を呼び出します。 - 条件付きの表示は、
&&
演算子を用いて以下のように実装します。
{num > 123 && (
<Parent>
<Child />
</Parent>
)}
解答例
こちらの CodePen を参照してください。
問題 2
パスワード入力コンポーネントを作成します。
「見る(隠す)」ボタンで、入力値の表示を切り替えられます。
/* ??? */
を埋めて、コードを完成させてください。
(練習のため、コピペではなく写して書きながら埋めるとよいです。)
function Password(/* ??? */) {
/* ??? */
}
function App() {
const [val, setVal] = React.useState('');
const handleChange = e => setVal(e.target.value);
return (
<>
<p>パスワード</p>
<Password value={val} onChange={handleChange} />
<p>{val.length}文字</p>
</>
);
}
const root = document.getElementById('root');
ReactDOM.render(<App />, root);
ヒント
- 入力値は親子どちらの state で管理されるでしょうか。二重管理する必要はありません。
- 表示の切り替えは、
<input>
のtype
属性値を切り替えて実現します。現在のtype
値は、Password
の状態として管理しましょう。
解答例
こちらの CodePen を参照してください。
問題 3
数あてゲームを作成しましょう。
ルールは以下の通りです。
- 1〜50までのランダムな正解数値が割り当てられる。
- 入力欄に数字を入れて「予想する」ボタンをクリックすると、正解か、もしくは予想が正解より大きいか小さいかが表示される。
- 5回までの予想でに正解できないと終了のメッセージが表示される。
- 「はじめから」ボタンをクリックすると、正解数値、残り予想回数、メッセージ表示がそれぞれリセットされる。
/* ??? */
を埋めて、コードを完成させてください。
(練習のため、コピペではなく写して書きながら埋めるとよいです。)
const random = (max) => {
return Math.floor(Math.random() * Math.floor(max)) + 1;
};
function Guess(/* ??? */) {
/* ??? */
}
function NumberGuessing() {
const max = 50;
const initialCount = 5;
const [answer, setAnswer] = React.useState(random(max));
const [count, setCount] = React.useState(initialCount);
const [message, setMessage] = React.useState('');
const judge = num => {
if (count === 0) return;
setCount(count - 1);
if (num === answer) {
setMessage('正解です!');
} else if (count === 1) {
setMessage('残念でした! 正解は' + answer);
} else if (num > answer) {
setMessage('もっと小さいです');
} else if (num < answer) {
setMessage('もっと大きいです');
}
};
const replay = () => {
setAnswer(random(max));
setCount(initialCount);
setMessage('');
};
return (/* ??? */);
}
const root = document.getElementById('root');
ReactDOM.render(<NumberGuessing />, root);
ヒント
- 入力欄と予想ボタンを
Guess
コンポーネントとします。 - 入力値は
Guess
コンポーネント内で管理することにします。 - 正誤判定(
judge
)は親コンポーネントの役割にします。
コード解説
以下の state の更新タイミングは注意が必要です。
setCount(count - 1);
// 中略...
} else if (count === 1) {
setMessage('残念でした! 正解は' + answer);
setCount
を実行している行で残回数をカウントダウンしているので、その下のゲームオーバー判定行では残りゼロかどうかを見ればいいようにも思えますが、そうではありません。
state 更新による再レンダリングは、処理がすべて終わってから実行されます。state 更新関数が呼ばれた直後に実行されて、そのあと次の行に戻ってくるわけではないです。
state を参照するための変数も、更新関数呼び出し直後に参照しても更新はされていません。再レンダリング後に参照して初めて更新されています。
というわけで、ゲームオーバー判定行では残回数 count
の値はまだ引き算される前なので、ゼロではなくその一つ前で判定しています。
最初は間違えやすいポイントだと思いますが、上で説明した state の更新サイクルを把握すれば対応できるでしょう。
解答例
こちらの CodePen を参照してください。