【React】state構造の原則をおさらいしてstateの使い方を反省する
概要
「このstateいる?」や「このstate更新しずら!」と感じる場面は多々あると思います。
いくつstateを使うのか、データ構造をどのようにするのかについて言語化が難しく、もっと考えてstateを使わなければといけないと感じたため、今一度、原点に立ち返りつつ、state構造の原則をおさらいして反省したいと思います。
state構造の原則
state構造の原則については公式から5つの原則が設けられており、これらの原則の背景にはミスを入りこませずにstateを容易に更新できるようにすることが目的となっている。
最適とはいえないstate構造でも動くコードを作ることは可能ではあるが、冗長で重複するデータを取り除くことで、関連するstateが同期した状態を保てるようになる。
関連するstateはグループ化する
複数のstateが常に同時に更新される場合は、それらはオブジェクトとして一つのstateにまとめることで、管理を容易にし、不整合を防ぐことができる。
個別に管理する場合
const [a, setA] = useState(0)
const [b, setB] = useState(0)
個別に管理する問題点として、aとbが常に同時に更新される必要がある場合、二つの set 関数の呼び出しにより、更新の整合性を管理するのが煩雑になる可能性がある。 また、更新ロジックが複雑になると、バグや不整合が生じるリスクが高まる。
オブジェクトでグループ化する場合
const [value, setValue] = useState({ a: 0, b: 0 })
aとbを同時に更新する場合、オブジェクト全体をまとめて更新できる。この場合の、メリットとして
setValue(prevValue => ({
...prevValue,
a: prevValue.a + 1,
b: prevValue.b + 1
}));
更新の簡素化が可能になり、一度のset関数呼び出しで複数の状態を更新できるため、パフォーマンスが向上することがある。(再レンダリングの回数が減る)
個別に管理するべきケース
- 各stateが独立している場合
- 変更頻度が異なる場合: あるstateは頻繁に変わり、もう一方のstateは滅多に変わらない場合は、個別に管理した方が効率的な場合がある。
stateの矛盾を避ける
stateの矛盾とは、複数のstateの更新タイミングのミスなどで、矛盾したstateが出来上がってしまうことである。
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);
送信中と送信完了を管理するisSending
とisSent
というstateがあるとする。
この場合、更新タイミングのミスなどで、isSendingとisSentがどちらもtrueになるという矛盾が生じる危険性がある。
この場合の対処として、一つのstateでstatusを管理するのが適切である。
const [status, setStatus] = useState<'typing' | 'sending' | 'sended'>('typing');
冗長なstateを避ける
レンダー中にpropsや既存のstateから情報を計算できる場合、その情報をstateで管理するべきではない。
複数のstateの更新タイミングのミスなどで、矛盾したstateが出来上がってしまう危険がある。
例えば、以下のように金額、量、総額のstateが存在していたとする。
const [price, setPrice] = useState(100);
const [quantity, setQuantity] = useState(2);
const [totalPrice, setTotalPrice] = useState(200);
この場合、priceとquantityのどちらかが更新されたタイミングでtotalPriceも更新させないとstateに矛盾が生じてしまう。
このように状態を計算で導出できる場合はstateに保存しないのが適切である。(そもそもstateで管理しない)
const [price, setPrice] = useState(100)
const [quantity, setQuantity] = useState(2)
const totalPrice = useMemo(() => price * quantity, [price, quantity])
state内の重複を避ける
整合性の観点から同じデータが複数のstateに存在しないように設計するべきである。
例えば、メニューが管理されているstate(items)と選択したメニューを管理しているstate(selectedItem)が存在していたとする。
const initialItems = [
{ title: 'pretzels', id: 0 },
{ title: 'crispy seaweed', id: 1 },
{ title: 'granola bar', id: 2 },
];
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(items[0]);
このstate管理の懸念としては、例えばitemsのtitleが更新されたタイミングでselectedItemのtitleも更新する必要があり、不整合が起こる危険性がある。
この場合の対処としては、更新されないidをstate管理し、そこからselectedItemを抽出することである。
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(0);
const selectedItem = useMemo(
() => items.find((item) => item.id === selectedId),
[items, selectedId],
)
深くネストされたstateを避ける
深くネストされたstateの場合、更新の際に親オブジェクトや配列をコピーし、そこから対象のプロパティを更新する必要がある。ネストが深くなるほど、更新ロジックが煩雑になり、ミスが発生しやすくなります。
const initialState = {
name: 'John',
address: {
street: '123 Main St',
city: 'Somewhere',
zip: '12345'
},
preferences: {
notifications: [
{ type: 'email', enabled: true },
{ type: 'sms', enabled: false },
{ type: 'push', enabled: true }
]
},
orders: [
{
id: 1,
items: [
{ name: 'Laptop', price: 999.99 },
{ name: 'Mouse', price: 25.99 }
]
},
{
id: 2,
items: [
{ name: 'Keyboard', price: 49.99 },
{ name: 'Monitor', price: 199.99 }
]
}
]
};
この場合は、useReducer
を使って可読性を上げることも考えられるが、正規化させることが推奨されている。
const normalizedState = {
name: 'John',
address: {
id: 1,
street: '123 Main St',
city: 'Somewhere',
zip: '12345'
},
notifications: {
byId: {
1: { id: 1, type: 'email', enabled: true },
2: { id: 2, type: 'sms', enabled: false },
3: { id: 3, type: 'push', enabled: true }
},
allIds: [1, 2, 3]
},
orders: {
byId: {
1: {
id: 1,
items: [1, 2] // item IDs
},
2: {
id: 2,
items: [3, 4] // item IDs
}
},
allIds: [1, 2]
},
items: {
byId: {
1: { id: 1, name: 'Laptop', price: 999.99 },
2: { id: 2, name: 'Mouse', price: 25.99 },
3: { id: 3, name: 'Keyboard', price: 49.99 },
4: { id: 4, name: 'Monitor', price: 199.99 }
},
allIds: [1, 2, 3, 4]
}
};
sms通知を変更する例
const updateSmsNotification = (newSetting) => {
setState(prevState => ({
...prevState,
notifications: {
...prevState.notifications,
byId: {
...prevState.notifications.byId,
2: {
...prevState.notifications.byId[2],
enabled: newSetting
}
}
}
}));
};
Discussion