Reactのステート管理 5 つの原則
TL;DR
5 つの原則
- 常に一緒に変わる state は、一つにまとめる
- 矛盾しないように、関連する state は管理する
- 既存の state や props から計算できる値は state にしない
- 重複を避ける
- 深いネストを避け、フラットにする
はじめに
Structuring state well can make a difference between a component that is pleasant to modify and debug, and one that is a constant source of bugs.
ステートをうまく構造化することで、修正やデバッグがしやすいコンポーネントと、バグの絶えないコンポーネントの違いが生まれます。(DeepL 翻訳)
Choosing the State Structure
最近、React アプリケーションの state の重複と、それに伴う useEffect()
の不適切な使用のリファクタリングを行う機会がありました。
その際に、Choosing the State Structure の記事で書かれている 5 つの原則を事前に意識できていれば、このような負債はある程度防げたはずであると思いました。
この記事では、その 5 つの原則を広めることを目的としています。
元記事の重要なポイントを基にしていますが、よりアクセスしやすいよう筆者なりに整理しました。したがって、筆者の解釈に基づく表現が含まれていることをご了承ください。
5 つの原則
1. 常に一緒に変わる state は、一つにまとめる
以下の座標の例のように、いつも一緒に変化する値は、1 つの state で管理しましょう。
そうすることで、値の同期に同期に悩まされることがなくなります。
// 👎 `x` と `y` は常に一緒に変化するが、別々の state で管理されている
- const [x, setX] = useState(0);
- const [y, setY] = useState(0);
// 👍 1 つの state にまとめる
+ const [position, setPosition]=useState({ x: 0, y: 0 });
2. 矛盾しないように、関連する state は管理する
Avoid contradictions in state より
状態が矛盾しないよう、どのような state にどんな値を保持するか考えましょう。
矛盾が起きてしまう状況を防ぐことで、バグのリスクを減らすことができます。
export default function FeedbackForm() {
// 👎 `isSending` と `isSent` が同時に `true` になることはないが、そのあり得ない状態が可能となっている
- const [isSending, setIsSending] = useState(false);
- const [isSent, setIsSent] = useState(false);
// 👍 有効な値のうちの 1 つを持つ、1 つの state に置き換える('typing'(初期状態)、'sending'、'sent' のいずれか)
+ const [status, setStatus] = useState('typing');
async function handleSubmit(e) {
e.preventDefault();
- setIsSending(true);
+ setStatus('sending');
await sendMessage(text);
- setIsSending(false);
- setIsSent(true);
+ setStatus('sent');
}
...
3. 既存の state や props から計算できる値は state にしない
不要な state 管理を減らすことで、コードがシンプルになります。
計算できる値はレンダリング時にその都度計算することで、同期が取れなくなる心配がなくなり、複雑度もバグのリスクも減らせます。
export default function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// 👎 `fullName` は `firstName` と `lastName` から計算できるが、state としている
- const [fullName, setFullName] = useState('');
// 👍 レンダリング時に `fullName` を計算する
+ const fullName = firstName + ' ' + lastName;
function handleFirstNameChange(e) {
setFirstName(e.target.value);
- setFullName(e.target.value + ' ' + lastName);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
- setFullName(firstName + ' ' + e.target.value);
}
...
}
このような冗長な state は、useEffect() の誤った使い方を誘発する例でもあります😣
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
...
// 👎 `fullName` state を `useEffect()` を使って更新する
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
...
You might not need an effect より
useEffect()
の誤った利用は、余分な再レンダリングによりアプリケーションが遅くなり、必要以上に複雑化し、同期ミスによるバグを生む可能性が生みます。
4. 重複を避ける
重複している state を減らすことで、管理が楽になります。必要な情報だけを state で保持しましょう。
const initialItems = [
{ title: 'pretzels', id: 0 },
{ title: 'crispy seaweed', id: 1 },
{ title: 'granola bar', id: 2 },
];
export default function Menu() {
const [items, setItems] = useState(initialItems);
// 👎 `items` state が持つ値の 1 つと同じオブジェクトが、`selectedItem` に保持されているので重複が起こっている
// `items = [{ id: 0, title: 'pretzels'}, ...]`
// `selectedItem = { id:0, title: 'pretzels'}`
- const [selectedItem, setSelectedItem] = useState(items[0]);
// 👍 `selectedId` を state に保持することで、重複を避け必要な状態だけを保持している
// `items = [{ id: 0, title:'pretzels'}, ...];
// `selectedId = 0`
+ const [selectedId, setSelectedId] = useState(0);
// 👍 items配列からそのIDを持つitemを検索してselectedItemを取得
+ const selectedItem = items.find(item =>
+ item.id === selectedId
+ );
function handleItemChange(id, e) {
setItems(items.map(item => {
if (item.id === id) {
return {
...item,
title: e.target.value,
};
} else {
return item;
}
}));
// 👎 `selectedItem` の更新を行わないとバグになる
- setSelectedItem(...);
}
return (
<>
<h2>What's your travel snack?</h2>
<ul>
{items.map((item, index) => (
<li key={item.id}>
<input
value={item.title}
onChange={e => {
handleItemChange(item.id, e)
}}
/>
{' '}
<button onClick={() => {
setSelectedItem(item);
}}>Choose</button>
</li>
))}
</ul>
<p>You picked {selectedItem.title}.</p>
</>
);
}
5. 深いネストを避け、フラットにする
複雑なネストは管理が難しくなりがちです。可能であれば、データ構造を見直し、フラットな構造にしましょう。
// 👎 ネストされた構造
- export const initialTravelPlan = {
- id: 0,
- title: '(Root)',
- childPlaces: [{
- // 惑星レベル
- id: 1,
- title: 'Earth',
- childPlaces: [{
- // 大陸レベル
- id: 2,
- title: 'Africa',
- childPlaces: [{
- // 国レベル
- id: 3,
- title: 'Botswana',
- childPlaces: []
- }, {
- ...
- }],
- }, {
- // 大陸レベル
- ...
- }],
- }, {
- // 惑星レベル
- ...
- }],
- };
// 👍 フラット化
+ 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]
+ },
+ 3: {
+ id: 3,
+ title: 'Botswana',
+ childIds: []
+ },
+ ...
+ };
まとめ
5 つの原則
- 常に一緒に変わる state は、一つにまとめる
- 矛盾しないように、関連する state は管理する
- 既存の state や props から計算できる値は state にしない
- 重複を避ける
- 深いネストを避け、フラットにする
元記事の最後の課題セクションでは、これらの原則が体系的に理解できるので、チャレンジしてみることをお勧めします!
Happy coding! 🚀
If you want to read this article in English, here is the link: https://dev.to/takuyakikuchi/five-principles-of-react-state-management-3l1n
Discussion