🗂

状態の設計について(React)

2023/03/11に公開

状態をの構造を検討するための原則

なんらかの状態を保持するコンポーネントを作成する場合、よりよい洗濯を行うための指針となる原則がいくつかあります。

  1. 状態のグループ化:常に 2 つ以上の状態変数を同時に更新する場合は、それらを 1 つの状態変数にマージすることを検討してください。
  2. 状態の矛盾の回避:状態のいくつかの部分が矛盾し、互いに「同意しない」ように状態が構造化されている場合、間違いの余地が残ります。これを避けるようにしてください。
  3. 冗長状態の回避:レンダリング中にコンポーネントの props または既存の状態変数から何らかの情報を計算できる場合、その情報をそのコンポーネントの状態に入れるべきではありません。
  4. 状態の重複の回避:複数の状態変数間、またはネストされたオブジェクト内で同じデータが重複している場合、同期を維持することは困難です。可能な場合は重複を減らします。
  5. 入れ子が深い状態の回避:階層が深い状態は、更新するのにあまり便利ではありません。可能であれば、フラットな方法で状態を構造化することをお勧めします。

1. 状態のグループ化

いくつかの 2 つの状態変数が常に一緒に変化する場合は、それらを 1 つの状態変数に統合することをお勧めします。

const [x, setX] = useState(0);
const [y, setY] = useState(0);const [position, setPosition] = useState({ x: 0, y: 0 });

x,y 座標を同時に扱う場合は状態を分ける必要はなく、同一の状態で扱いましょう。

2. 状態の矛盾の回避

const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);

async function handleSubmit(e) {
  e.preventDefault();
  setIsSending(true);
  await sendMessage(text);
  setIsSending(false);
  setIsSent(true);
}

上記のコードは機能しますが、setIsSentsetIsSending を一緒に呼び出すのを忘れると、isSendingisSent の両方が同時に true になる状況になる可能性があります。
これは、バグの可能性を残してしまいます。

ここで、sending→sent になる状態を宣言してあげると矛盾を解消できます。

const [status, setStatus] = useState("typing");

async function handleSubmit(e) {
  e.preventDefault();
  setStatus("sending");
  await sendMessage(text);
  setStatus("sent");
}

これで矛盾が解消されます。sending の状態と sent の状態が同時に存在する可能性がなくなりました。

3. 冗長状態の回避

レンダリング中にコンポーネントの props または既存の状態変数から何らかの情報を計算できる場合、その情報をそのコンポーネントの状態に入れるべきではありません。
e.g.

const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [fullName, setFullName] = useState("");

function handleFirstNameChange(e) {
  setFirstName(e.target.value);
  setFullName(e.target.value + " " + lastName);
}

function handleLastNameChange(e) {
  setLastName(e.target.value);
  setFullName(firstName + " " + e.target.value);
}

fullNameの状態は、firstName や lastName のレンダリング中にそれぞれの状態を使用しているので無駄なレンダリングが走ります。

const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");

const fullName = firstName + " " + lastName;

function handleFirstNameChange(e) {
  setFirstName(e.target.value);
}

function handleLastNameChange(e) {
  setLastName(e.target.value);
}

これで無駄な計算(レンダリング)がなくなります。

4. 状態の重複の回避

最初にアイテムの [選択] をクリックしてから編集すると、入力は更新されますが、下部のラベルには編集が反映されないことに注意してください。
これは、状態が重複していて、selectedItem を更新するのを忘れるためです。

const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(items[0]);

setSelectedItem した時の items の状態を入れることになり、items のオブジェクトの情報を二箇所で持つことになり、重複しています。

const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(0);

const selectedItem = items.find((item) => item.id === selectedId);

こうすることで、items 単体でオブジェクトの状態を管理しており、重複の解消を行えております。

状態は、次のように複製されていました。

items = [{ id: 0, title: 'pretzels'}, ...]
selectedItem = {id: 0, title: 'pretzels'}

しかし、変更後は次のようになります。

items = [{ id: 0, title: 'pretzels'}, ...]
selectedId = 0

5. 入れ子が深い状態の回避

惑星・大陸・国からなるネストされたオブジェクトを表示します。

状態がネストされすぎて簡単に更新できない場合は、「フラット」にすることを検討してください。
状態にオブジェクトや配列が入るときに特に意識してください。
useState で宣言する時のデータそのものを整形しましょう。

export const initialTravelPlan = {
  0: {
    id: 0,
    title: '(Root)',
    childIds: [1, 43, 47],
  },
  1: {
    id: 1,
    title: 'Earth',
    childIds: [2, 10, 19, 27, 35]
  },
  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: []
  },
  5: {
    id: 5,
    title: 'Kenya',
    childIds: []
  },
  ...
}

状態が「フラット」(「正規化」とも呼ばれる) になったため、ネストされたアイテムの更新が容易になります。
ここで場所を削除するには、次の 2 つのレベルの状態を更新するだけで済みます。

  • 親 place の更新されたバージョンは、削除された ID をその childIds 配列から除外する必要があります。
  • ルート「テーブル」オブジェクトの更新バージョンには、親プレースの更新バージョンが含まれている必要があります。

ステートは好きなだけ入れ子にすることができますが、ステートを「フラット」にすることで多くの問題を解決できます。
状態の更新が容易になり、ネストされたオブジェクトのさまざまな部分で重複が発生しないようにするのに役立ちます。
場合によってはネストされている状態の一部を子コンポーネントに移動することもできます。
これは、アイテムがホバーされているかどうかなど、保存する必要のない一時的な UI 状態に適しています。

余談(props を State の初期値に入れるのはやめましょう)

function Message({ messageColor }) {
  const [color, setColor] = useState(messageColor);

props として新しい状態を定義していますが、親コンポーネントで messageColor が変更になってもcolorの状態は変更されません。
(レンダリングが起きてないので状態が変更されないです。)

function Message({ messageColor }) {
  const color = messageColor;

props を state に「ミラーリング」することは、特定の props のすべての更新を無視したい場合にのみ意味があります。

function Message({ initialColor }) {
  // The `color` state variable holds the *first* value of `initialColor`.
  // Further changes to the `initialColor` prop are ignored.
  const [color, setColor] = useState(initialColor);
GitHubで編集を提案

Discussion