前回までのあらすじ
以前の記事でstateの基礎知識に関する内容をまとめました。そこでstateがどういう雰囲気のものなのかという概念を学び、基本的な使い方について触れました。
今回はそんなstateの本格的?な使い方を学びます。
JavaScriptすらちゃんと触った経験が浅いのにまだまだいっぱい覚えることあるけど、大丈夫かなぁ…。大丈夫かなぁじゃないねん、強い気持ちで覚えるっ。
なお、今回も公式のチュートリアルサイトを参考にしており、そちらで学んだことのまとめ記事となりますので詳しくは公式チュートリアルをご参照ください。
stateの管理基礎
state を使って入力に反応する
いきなりですが、Reactは宣言型プログラミングである。
宣言型プログラミングとは、UI を細かく管理する(命令型)のではなく、視覚状態ごとに UI を記述することを意味する。
例えばjsだとイベントごとにUIの処理を分けて書くが、Reactは全パターンのUIを記述し、イベントハンドラで視覚状態を切り替え、視覚状態ごとに表示を分ける。
簡単にいうとjsは相対的な指示で、Reactは絶対的な指示である。
視覚状態とは例えば「入力中」や「送信中」などであり、例えば以下のようにして編集中かどうかを管理できる
const [isEditing, setIsEditing] = useState(false); // 視覚状態
const [name, setName] = useState('Jane');
// ...
return (
// ...
{isEditing ? ( // 編集中なら
<input
value={name}
onChange={e => {
setName(e.target.value)
}}
/>
) : ( // そうでなければ
<b>{name}</b>
)}
// ...
)
state 構造の選択
stateを定義する際、意識すべきことがいくつかある。
関連する state をグループ化する
グループ化とは、オブジェクトや配列にまとめることを指す。
具体的には、座標などの常にセットになっている値や、事前にstateの数が決まっていないものはグループ化すべしということ。
state の矛盾を避ける
例えば送信中と送信済の二つの状態をstateで管理するとき、
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);
と二つ定義すると、考慮漏れなどで二つともがTrueになった場合矛盾を起こしてエラーが起こりかねないため一つのstateにまとめるべし。
冗長な state を避ける
例えば配列とその要素数を管理する場合
const initialItems = [
{ id: 0, title: 'Warm socks' },
{ id: 1, title: 'Travel journal' },
{ id: 2, title: 'Watercolors' },
];
export default function TravelPlan() {
const [items, setItems] = useState(initialItems); // 配列のstate
const [total, setTotal] = useState(items.length); // itemsの合計数のstate
...
お互い連動するはずのitems
とtotal
が両方stateになっているのはよくない。
このままだとitems
を増やせばtotal
もsetTotal
で更新しないといけない。
items
とtotal
は一心同体なので矛盾を避けるためにもstateにしないほうがいい。
// ...省略
export default function TravelPlan() {
const [items, setItems] = useState(initialItems);
const total = items.length;
これでitems
の数が増減すると自動的にtotal
が更新されるようになる。
state 内の重複を避ける
以下のように、同じオブジェクトを使って複数のstateを管理するのはよくない。
const initialItems = [
{ title: 'pretzels', id: 0 },
{ title: 'crispy seaweed', id: 1 },
{ title: 'granola bar', id: 2 },
];
...
const [items, setItems] = useState(initialItems); // initialItemsのstate
const [selectedItem, setSelectedItem] = useState( // initialItemsの一要素のstate
items[0]
);
これだと両方のstateを一緒に変更しなければ、selectedItem
がitems
の要素に一致しない。
同じオブジェクトを継承しているからといって同期されるわけではない。(前回で触れたように、stateは定義された時点で作り替えられて独立して存在するものだから。)
全アイテムと選んだアイテムを管理するなら、以下のように対応できる。
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(0); // 要素は管理せずIDだけ管理する
const selectedItem = items.find(item =>
item.id === selectedId
);
このように、データに矛盾が起きないようにオブジェクトは一括管理することを心がけよう。
よく考えたらReactでなくてもこの考え方は当然のものか。。
深くネストされた state を避ける
例えばこんな深いネストのオブジェクトはstateに含めては大変。
export const initialTravelPlan = {
id: 0,
title: '(Root)',
childPlaces: [{
id: 1,
title: 'Earth',
childPlaces: [{
id: 2,
title: 'Africa',
childPlaces: [{
id: 3,
title: 'Botswana',
childPlaces: []
}, {
id: 4,
title: 'Egypt',
childPlaces: []
}, {
...
実際、親・子の関係であるデータを扱うときにこうしたい気持ちはわかるが、このstateに変更を加えるとき、変更する箇所から上位レベル全てのオブジェクトをコピーしないといけない。(stateのオブジェクトが持つ情報は最上位レベルの構造のみだから。)
つまりフラットであることが最善であるのだ。
以下のようにすればフラットになる
export const initialTravelPlan = {
0: {
id: 0,
title: '(Root)',
childIds: [1, 42, 46], // 子オブジェクトのIDを持つことで親子関係が保たれる
},
1: {
id: 1,
title: 'Earth',
childIds: [2, 10, 19, 26, 34]
},
2: {
id: 2,
title: 'Africa',
childIds: [3, 4, 5, 6 , 7, 8, 9]
},
3: {
id: 3,
title: 'Botswana',
childIds: []
},
4: {
id: 4,
title: 'Egypt',
childIds: []
},
...
とにかく、stateはできる限り簡素にするんだという強い気持ちを持つ。
コンポーネント間で state を共有する
複数のコンポーネントにまたがって状態管理をしたいことはザラにあるだろう。複数のコンポーネントを協調動作させたい場合は、state を共通の親で定義すればよろしい。
そして、その共通の親から props 経由で情報を下に渡す。
最後に、子が親の state を変更できるよう、イベントハンドラを下に渡す。
例:First input
に文字を入力すると、同時にSecond input
にも反映されるようにする
import { useState } from 'react';
export default function SyncedInputs() {
const [text, setText] = useState(''); // ここでstateを定義(Input関数内ではなく!)
function handleChange(e) { // イベントハンドラもここで定義
setText(e.target.value);
}
return (
<>
<Input
label="First input"
value={text}
onChange={handleChange}
/>
<Input
label="Second input"
value={text}
onChange={handleChange}
/>
</>
);
}
function Input({ label, value, onChange }) { // stateとイベントハンドラを受け取るだけ
return (
<label>
{label}
{' '}
<input
value={value}
onChange={onChange}
/>
</label>
);
}
state の保持とリセット
いきなりだが、stateの独立単位はコンポーネントである。
JSXタグが一つだろうが複数だろうが、はたまた条件分岐でコンポーネントを返していようが、最終的なツリーで同じ位置にあるコンポーネントは同じものと見なされてその中にあるstateは保持されるし、別のコンポーネントとして定義されたらそれは別のstateとして新たに生成される。
つまりコンポーネントは、要素の順番や構成によって同一物かどうかを判断される。
以下の例では、とあるstateの定義を含むFormを表示し、二つの場合によって表示を分けている。
ただしFormは場合によって表示が変わってはいけないという状況。
ポイントは、else
の方の{null}
がなければstateが保持されないということ。
if (flag) {
return (
<div>
<p><i>flagがTrueならこの文章が表示される〜</i></p>
<Form /> {/* div内の2番目の要素 */}
...
);
} else { // わかりやすさのためelseで記述
return (
<div>
{null} {/* これがないと上とは別のコンポーネントとみなされる */}
<Form /> {/* div内の2番目の要素 */}
...
);
}
では、同じツリー構造だけどstateを分けて管理したい場合はどうか。
結論、stateを保持するJSXタグにkeyを与えるとkeyごとに区別してstateを保持できる。
逆に言えば、異なる key を与えることでサブツリーの state をリセットするよう強制することができる。
以下の例では、同じJSXタグを使って同じツリー構造になっているにも関わらず、別々で管理される。
function Field({ label }) {
const [text, setText] = useState('');
return (
...
}
...
<Field key="lastName" label="Last name" /> // keyで区別してstateを管理できる
<Field key="firstName" label="First name" />
...
ちなみに普通のタグにもkeyを与えられる
<img key={image.id} src={image.src} />
これは例えば画像の読み込みに時間がかかる画面遷移を行ったとき、イベントハンドラによってstateが更新されてテキストは更新されたのに、画像だけしばらく前の画像が表示されてしまうのを防ぐのに有効。
keyとstateを紐づけておけば、stateが更新された瞬間に画像は一旦削除される。
あと、state定義を持つコンポーネント定義をネストさせてはいけない。さもないと上位のコンポーネントがレンダーされるたびににそれより下の階層の state が全てリセットされてしまう。
import { useState } from 'react';
export default function MyComponent() {
const [counter, setCounter] = useState(0);
function MyTextField() {
const [text, setText] = useState(''); // MyComponentがレンダーされるたびにリセットされてまうで
return (
<input
value={text}
onChange={e => setText(e.target.value)}
/>
);
}
まとめ
一回理解すると楽しいです。
ようやく公式チュートリアルのチャレンジ問題も解けるようになってきました。
次回はstateの管理応用編です。