【6日目】React初学者が1週間でどこまで出来るか挑戦(イベントとState)
はじめに
昨日、一昨日と予定があり日が空いてしまいましたが、React学習1週間チャレンジ6日目です。
前回まででInertiaのへルパなどを使ってCreateフォームができました。しかしInertiaがいい感じに処理してくれるため、ReactのuseStateなどよく分からないままでした。そこで今回は改めてReactドキュメントに沿ってイベントハンドラやState管理を学びました。
Adding Interactivity の各チャプタを一気に読み切りました。要所は自分のコードに置き換えて理解を深めています。
Responding to Events
ドキュメント: Responding to Events – React
イベントハンドラ関数
イベントハンドラ関数は
- 通常はコンポーネント内で定義される
-
handle
から始まるイベント名で命名する(例:handleClick
)- 他の命名規則でも動作はする(例:
testClick
)
- 他の命名規則でも動作はする(例:
- インラインで関数も書くことができる
- アロー関数を使って書くこともできる
export default function Button() {
function handleClick() {
alert('handle + イベント名 が通常の命名規則');
}
function testClick() {
alert('他の命名規則でもOK');
}
return (
<>
<button onClick={handleClick}>handleClick</button>
<button onClick={testClick}>testClick</button>
<button onClick={function inlineClick () {
alert('インラインは短い関数を書くのに便利!');
}}>inlineClick</button>
<button onClick={() => {
alert('アロー関数も使えます');
}}>arrowClick</button>
</>
);
}
イベントハンドラでpropsを使う
function ActionButton({ message, children }) {
return (
<button onClick={() => alert(message)}>
{children}
</button>
);
}
export default function OperationBar() {
return (
<>
<ActionButton message="更新します">
更新
</ActionButton>
<ActionButton message="削除します">
削除
</ActionButton>
</>
);
}
イベントハンドラをpropsとして渡す
例えば、ある共通の部品を以下のような条件で作りたいとする。
- 同じ見た目のボタンであること
- クリックするとクリックイベントを起こすこと
- ただしクリックイベントの挙動は個別に決めたい
次のように親コンポーネント(Button)にonClick
のようにイベントハンドラを渡す。
function Button({ onClick, children }) {
return (
<button onClick={onClick}>
{children}
</button>
);
}
function StoreButton({ targetName }) {
function handleStoreClick() {
alert(`${targetName}を登録します。`)
}
return (
<Button onClick={handleStoreClick}>
{targetName} 登録
</Button>
);
}
function UpdateButton() {
return (
<Button onClick={() => alert('更新します')}>
更新
</Button>
);
}
function DeleteButton() {
function handleDeleteButton() {
if (window.confirm('本当に削除しますか?')) {
// 削除処理省略
console.log('Item has been deleted.');
}
}
return (
<Button onClick={handleDeleteButton}>削除</Button>
);
}
export default function OperationBar() {
return (
<>
<StoreButton targetName="ユーザー" />
<StoreButton targetName="顧客" />
<UpdateButton />
<DeleteButton />
</>
);
}
イベントハンドラpropsの命名
-
<button>
や<div>
などビルトインコンポーネントはonClick
などのブラウザイベント名のみ対応している - コンポーネントを作る時に、独自のイベントハンドラpropsを命名することもできる
-
on
から始めるのが慣例(例:onSmash
)
-
上記のサンプルコードをonClick
から独自のonSmash
に書き換えると以下のようになる。
function Button({ onSmash, children }) {
return (
<button onClick={onSmash}>
{children}
</button>
);
}
function StoreButton({ targetName }) {
function handleStoreClick() {
alert(`${targetName}を登録します。`)
}
return (
<Button onSmash={handleStoreClick}>
{targetName} 登録
</Button>
);
}
// ... 省略
ブラウザ対応の<button>
タグではonClick
が必要だが、自分で定義したButton
コンポーネントにはonSmash
が使える。
その他
イベントの伝播、伝播の止め方、ブラウザのデフォルト動作の止め方についてJavaScriptの知識でカバーできそうなため詳細は省略。
State: A Component's Memory
ドキュメント: State: A Component's Memory – React
useStateの理解
import { useState } from 'react';
const [state, setState] = useState(initialState);
- レンダリング間で情報を保持するためには状態変数(state variable)を使う
- 状態変数は
useState
フック(Hook)を使って宣言する - フックは
use
から始まる特別な関数 -
[something, setSomething]
の構文は配列の分割代入
useState使用例
ローカルの変数はレンダリング間で保持できない。例えば、以下のようにボタンを押すたびに変数index
をhandleClick
関数で1増やす処理を実行する。handleClick
関数内のconsole.log
の結果はインクリメントされているが、レンダリングされているIndex: {index}
の部分は変化がない。
export default function () {
let index = 0;
function handleClick() {
index = index + 1;
console.log(`index結果: ${index}`); // 1, 2, 3 ...と増える
}
return (
<>
Index: {index} {/* 変化無し */}
<button onClick={handleClick}>
増やす
</button>
</>
);
}
次のようにuseState
を使って管理すること解決できる。
import { useState } from "react";
export default function () {
const [index, setIndex] = useState(0);
function handleClick() {
setIndex(index + 1);
console.log(`index結果: ${index}`); // 0, 1, 2 ...と増える
}
return (
<>
Index: {index}. {/* 1, 2, 3 ...と増える */}
<button onClick={handleClick}>
増やす
</button>
</>
);
}
setIndex(index + 1);
の後にconsole.log
でindex
を確認すると、初回1
ではなく0
になるのは後のチャプターで出てくるが、状態は次のレンダリング時に更新されるからのようだ。
参照: State as a Snapshot – React
Stateは独立しPrivateである
次のコードのように、同じ状態変数を持ったコンポーネントを複数使った場合、状態はそれぞれのコンポーネントで独立して変化する。Button
コンポーネントが二つあり、増やすボタンを押すと押したボタンのIndexのみが増える。(同じ状態変数を使っていて連動しそうにも思えるが、独立していることが分かる)
import { useState } from "react";
function Button() {
const [index, setIndex] = useState(0);
function handleClick() {
setIndex(index + 1);
console.log(`index結果: ${index}`); // 0, 1, 2 ...と増える
}
return (
<>
Index: {index} {/* 1, 2, 3 ...と増える */}
<button onClick={handleClick}>
増やす
</button>
</>
);
}
export default function () {
return (
<>
<Button />
<Button />
</>
);
}
Render and Commit
ドキュメント: Render and Commit – React
レンダリングされるプロセスの理解。
- Trigger
- Render
- Commit
Step 1: Trigger a render
コンポーネントがレンダリングされる状況は2つある:
- 初期レンダリング
- コンポーネントの状態が更新された時
初期レンダリング
指定したDOMノードで createRoot
を使用してルートオブジェクトを生成し、そのルートオブジェクトの render
メソッドを使用してコンポーネントをレンダリングする。
// ドキュメントからそのまま抜粋
import Image from './Image.js';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'))
root.render(<Image />);
以前Inertiaを使ったセットアップではエントリーポイントのapp.jsx
に次のように記述していた。これでcreateRoot
の記述の意味が分かった。
// Inertiaの場合
// resources/js/app.jsx
import { createInertiaApp } from '@inertiajs/react'
import { createRoot } from 'react-dom/client'
createInertiaApp({
resolve: name => {
const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true })
return pages[`./Pages/${name}.jsx`]
},
setup({ el, App, props }) {
createRoot(el).render(<App {...props} />)
},
})
状態更新時
useState
などのset
関数でコンポーネントの状態が更新された時、再度レンダリングされる。
Step 2: Render components
- レンダリングをトリガーした後、Reactがコンポーネントを呼び出して画面に何を表示するか決める
- 「レンダリング」とはReactがコンポーネントを呼び出すこと
Step 3: Commits changes to the DOM
- コンポーネントをレンダリングすると、ReactはDOMを修正する
- レンダリング間の差分があったDOMノードだけを修正する
差分のみ修正される例はドキュメントのサンプルが分かりやすい: Render and Commit – React
同じコンポーネント内にh1
要素とinput
要素があるが、状態が更新されたh1
要素のみが変更されていることが分かる。
State as a Snapshot
ドキュメント: State as a Snapshot – React
Reactが再レンダリングする時の流れは、
- 関数を実行
- スナップショットを返す(一時的な、その時点の状態)
- 画面を更新(DOMを更新)
例えば、次のようにonClick
の処理で一度のクリックイベントの中にsetIndex
でインクリメントする処理を3回書いても、実際には一度のクリックで増えるのは3ではなく1だけ。関数が実行完了し次のレンダリングがされるまで見ているのはあくまでスナップショットということだと思う。
// Example for "State as a Snapshot"
import { useState } from "react";
export default function () {
const [index, setIndex] = useState(0);
return (
<>
Index: {index}.
<button onClick={() => {
setIndex(index + 1);
setIndex(index + 1);
setIndex(index + 1);
}}>
増やす
</button>
</>
);
}
Queueing a Series of State Updates
ドキュメント: Queueing a Series of State Updates – React
前のチャプターで見たsetIndex
を3回実行して3つ増やす結果を得るためには、以下のように書き換える。(あまり使わないユースケースかもしれないが)
<button onClick={() => {
setIndex(n => n + 1);
setIndex(n => n + 1);
setIndex(n => n + 1);
}}>
n => n + 1
の部分は updater functionと呼ばれる。
Updating Objects in State
ドキュメント: Updating Objects in State – React
ポイント:
- 状態(State)はオブジェクトも含むJavaScriptのどんな値も持つことができる
- ただし、オブジェクトを直接変更することはするべきじゃない
- 代わりにオブジェクトのコピーを用意して状態をセットする
mutationとは?
これまでの例では、数値、文字列、真偽値など、immutable(不変)な値を見てきた。
先の例ではconst [index, setIndex] = useState(0);
で状態をセットし、setIndex(index + 1);
によって、index
を0
から1
に変更した。しかし、0
という数字そのものが変わってしまったわけではない。
オブジェクトの場合、次のようにオブジェクトの中身は技術的には変更することが可能(mutable = 可変)。この変化をmutationと呼ぶ。(直訳すると「突然変異」)
import { useState } from "react";
export default function () {
const [object, setObject] = useState({ x: 0, y:0 });
return (
<>
x: {object.x} {/* 0のまま */}
<button onClick={() => {
object.x = 5;
console.log(object.x); // 5
}}>
クリック
</button>
</>
);
}
ただし、オブジェクトも数値や文字列、真偽値のようにimmutableな値として扱うべきである。setObject
関数によって、object
は新しいオブジェクトに置き換えられ、このコンポーネントを再度レンダリングする。
import { useState } from "react";
export default function () {
const [object, setObject] = useState({ x: 0, y:0 });
return (
<>
x: {object.x} {/* 5に変わる setObjectで状態を更新したため */}
<button onClick={() => {
setObject({
x: 5,
y: 0
});
console.log(object.x); // 0
}}>
クリック
</button>
</>
);
}
スプレッド構文でオブジェクトをコピー
次に、ユーザー入力フォームを例に考える。以下は、inputボックスに入力した内容を「入力内容欄」に表示したい例。ただし、うまくいかない。オブジェクトはname
、email
、address
と3つのプロパティを持っているが、onChange
の中でserUser
で状態を更新する時に、他のプロパティを指定していないため。
// 間違った例
import { useState } from "react";
export default function () {
const [user, setUser] = useState({
name: '',
email: '',
address: '',
});
return (
<>
<label>
名前:
<input type="text"
value={user.name}
onChange={(e) => setUser({name: e.target.value})}
/>
</label>
<label>
E-mail:
<input type="text"
value={user.email}
onChange={(e) => setUser({email: e.target.value})}
/>
</label>
<label>
住所:
<input type="text"
value={user.address}
onChange={(e) => setUser({address: e.target.value})}
/>
</label>
<div>
<h2>入力内容</h2>
<ul>
<li>名前: {user.name}</li>
<li>E-mail: {user.email}</li>
<li>住所: {user.address}</li>
</ul>
</div>
</>
);
}
以下のように修正。(ただし冗長)
// 修正版
import { useState } from "react";
export default function () {
const [user, setUser] = useState({
name: '',
email: '',
address: '',
});
return (
<>
<label>
名前:
<input type="text"
value={user.name}
onChange={(e) => setUser({
name: e.target.value,
email: user.email, // 元の値を維持
address: user.address, // 元の値を維持
})}
/>
</label>
<label>
E-mail:
<input type="text"
value={user.email}
onChange={(e) => setUser({
email: e.target.value,
name: user.name, // 元の値を維持
address: user.address, // 元の値を維持
})}
/>
</label>
<label>
住所:
<input type="text"
value={user.address}
onChange={(e) => setUser({
address: e.target.value,
name: user.name, // 元の値を維持
email: user.email, // 元の値を維持
})}
/>
</label>
<div>
<h2>入力内容</h2>
<ul>
<li>名前: {user.name}</li>
<li>E-mail: {user.email}</li>
<li>住所: {user.address}</li>
</ul>
</div>
</>
);
}
オブジェクトのスプレッド構文(...
)を使って改良。
// 他の部分は省略
<label>
名前:
<input type="text"
value={user.name}
onChange={(e) => setUser({
...user, // 元の値をコピー
name: e.target.value, // overrideする
})}
/>
</label>
<label>
E-mail:
<input type="text"
value={user.email}
onChange={(e) => setUser({
...user, // 元の値をコピー
email: e.target.value, // overrideする
})}
/>
</label>
<label>
住所:
<input type="text"
value={user.address}
onChange={(e) => setUser({
...user, // 元の値をコピー
address: e.target.value, // overrideする
})}
/>
</label>
Updating Arrays in State
ドキュメント: Updating Arrays in State – React
オブジェクトと同様に、配列はmutableだがimmutableとして扱うべき。元の配列を操作するではなく、新しい配列を作って操作すること。配列の扱いについて分かりやすい表があったため参照:
🆖 avoid (mutates the array) | 👍 prefer (returns a new array) | |
---|---|---|
adding | push , unshift |
concat , [...arr] spread syntax |
removing | pop , shift , splice |
filter , slice |
replacing | splice , arr[i] = ... assignment |
map |
sorting | reverse , sort |
copy the array first |