はじめに
このシリーズは、Reactの公式ページなどから特に重要と感じたトピックを選び出し、何度でも読み返せるよう簡潔にまとめたものです。
進捗管理には「いいね」か「ブックマーク」がお勧め
さらに僕のモチベーションアップになります!よろしくお願いいたします!
state構造の原則
コンポーネントでいくつstate変数を使うのか、データ構造の検討をすることも増えていくかと思います。
state構造の原則は5つあります。
ミスを入り込ませずにstateを容易に更新すること、全てのstateが同期した状態を保てることが主な目的です。
原則
1.関連するstateをグループ化する
2.stateの矛盾を避ける
3.冗長なstateを避ける
4.state内の重複を避ける
5.深くネストされたstateを避ける
1.関連するstateをグループ化する
2つ以上のstate変数を常に同時更新する場合は、単一のstateにまとめることを検討しましょう。
例えば、ポインタの座標をstateとする時は、一つのstateとしてまとめるとよいです。
//この場合、二つのstateを同時に更新する必要がある
const [x, setX] = useState(0);
const [y, setY] = useState(0);
//オブジェクトとして一つにまとめると一つに管理でき同時に更新可能
const [position, setPosition] = useState({ x: 0, y: 0 });
//オブジェクトにした場合、一つの値のみ変更するときはスプレット構文が必要なため注意
setPosition({ ...position, x: 100 })
2.stateの矛盾を避ける
どちらもtrueになることがありえない、stateを同時に変更しないと矛盾が生じる等の構成は避けましょう。
例えば、送信中のステータスと送信済みのステータスは別々に管理せず、一括で管理できるとよいです。
// 別々管理で、送信中でなくなり送信済みになった場合
await sendMessage(text); //API呼び出し
setIsSending(false);
setIsSent(true);
// stateでステータスを管理できるようにすれば一つで済む
setStatus('sending');
await sendMessage(text); //API呼び出し
setStatus('sent');
// さらに定数宣言すれば、別々管理の時のように判別しやすくなる
const isSending = status === 'sending';
const isSent = status === 'sent';
3.冗長なstateを避ける
stateを合算させて新たなstateを作成するなどの冗長構成は避けましょう
// [NG]
// fullNameは、firstName,lastNameの合算
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
// fullNameを更新するには、firstNameかlastNameの更新に合わせる必要が出てしまう
setFirstName(e.target.value);
setFullName(e.target.value + ' ' + lastName);
// [OK]
// firstNameかlastNameが更新関数で更新された場合は再レンダリングが走る
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// 再レンダリングが走るのでstateでなくてもconstで画面の描画はリアルタイムに変化する
const fullName = firstName + ' ' + lastName;
また、propsをstateにコピーするのはバグの温床にもなります。
初期レンダリングのみ値が欲しいのであれば問題ないですが、下記の書き方だと再レンダリングには対応しません。
// [NG]
// 初期レンダリングの時のみ、messageColorの値がコピーされる
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);
// [OK]
// 親コンポーネントと同期をとりたい場合は定数を使用すること。
function Message({ messageColor }) {
const color = messageColor;
4.state内の重複を避ける
上記と同様にstateのスナップショットを別のstateで管理すると複雑性が増します。
// [NG]
// アイテムリストのstate
const [items, setItems] = useState(initialItems);
// ボタンを押下した時に選択したitemをセットする
const [selectedItem, setSelectedItem] = useState(items[0]);
上記の場合、selectedItemとitemsは同期しているわけではないため、変更があった場合、値にずれが発生します。
配列のインデックスで対応可能ならば、stateの値を管理するのではなく、インデックス番号を管理する等で対応しましょう。
// [OK]
// アイテムリストのstate
const [items, setItems] = useState(initialItems);
// ボタンを押下した時にitemsのインデックスをセットする
const [selectedId, setSelectedId] = useState(0);
// 再レンダリングが走った時に再計算され定数がセットされるようになります
const selectedItem = items.find(item =>
item.id === selectedId
);
5.深くネストされたstateを避ける
以下の例のように、深くネストされた箇所を更新処理する場合(例えばid3のtitleを更新)、対象を指定するために、各階層においてスプレット構文を多用する必要が出てきます。
//Earth>Africa>Botswana
export const initialTravelPlan = {
id: 0,
title: '(Root)',
childPlaces: [{
id: 1,
title: 'Earth',
childPlaces: [{
id: 2,
title: 'Africa',
childPlaces: [{
id: 3,
title: 'Botswana',
childPlaces: []
},
更新処理の例
const [travelPlan, setTravelPlan] = useState(initialTravelPlan);
const updateTitle = (placeId, subPlaceId, id, newTitle) => {
setTravelPlan(
{
...travelPlan,
childPlaces: travelPlan.childPlaces.map(place =>
place.id === placeId ? {
...place,
childPlaces: place.childPlaces.map(subPlace =>
subPlace.id === subPlaceId ? {
...subPlace,
childPlaces: subPlace.childPlaces.map(subSubPlace =>
subSubPlace.id === id ? { ...subSubPlace, title: newTitle } : subSubPlace
)
} : subPlace
)
} : place
)
})
}
フラットな構造にすることで、更新処理の冗長性が減ります。
必ずしも全てがフラットである必要はありませんが、ネストが深くなりすぎると可読性に問題が生じるため、その際は構造を見直しましょう。
export const initialTravelPlan = {
0: {
id: 0,
title: '(Root)',
childIds: [1, 42, 46],
},
1: {
id: 1,
title: 'Earth',
childIds: [2, 10, 19, 26, 34]
},
2: {
id: 2,
title: 'Africa',
childIds: [3, 4, 5, 6 , 7, 8, 9]
},
更新処理の例
const updateTitle = (id, newTitle) => {
setTravelPlan(() => ({
...travelPlan,
[id]: {
...travelPlan[id],
title: newTitle
}
}))
}
参考サイト
より詳しく学びたい方はこちら