😺

React Docs BETAを読む 【状態構造の選択】

2022/07/08に公開

React Docs BETAを読む 【状態の管理】
React Docs BETAを読む 【Stateで入力に反応する】
に引き続き状態管理の章を読み進めていきます。

Choosing the State Structure

状態管理をする上で状態の構造を考えることは避けて通れません。
これまで度々出てきたように重複や冗長な状態はバグを生み出します。
この章では状態を構造化する際に考慮するべきヒントが書いてあります。

Principles for structuring state

状態の構造化の原則についての節です。
状態を構造化する際に参考になる指針が書いてあります。

  1. 関連する状態をグループ化する。
  2. 状態の矛盾を避ける。
  3. 冗長な状態を避ける。
  4. 状態の重複を避ける。
  5. ネストの深い状態を避ける。

それぞれわかりやすいですね。
この原則に従っていけば基本的によりシンプルな構造に向かっていくと思います。
そうすることで、保守性も高まりますし、ミスが起きづらくなります。
DBの正規化と似ていると書いてありますが、その通りだなと思いました。

原則1についての節です。
関連するステートはまとめて管理しましょうという話ですね。
例えば座標を状態として持つ場合を考えると以下のようになります。

//BAD 関連する状態が別々に管理されている。
const [x, setX] = useState(0);
const [y, setY] = useState(0);

//GOOD 関連する状態がまとめて管理されている。
const [position, setPosition] = useState({ x: 0, y: 0 });

ある状態変数が常に一緒に変化する場合は、1つに統合してしまったほうが良いかもしれないという話ですね。
もう1つのケースとして、カスタムフィールドなどの異なる状態がいくつ必要になるかわからない場合が挙げられています。

Avoid contradictions in state

原則2についての節です。
矛盾する状態を避けましょう。
状態の取りうる組み合わせを考えたときに、矛盾する組み合わせが存在するような管理はなるべく避けたほうがよいという話ですね。
機能的には問題ないんだけど、バグの追跡などがしづらくなります。
わかりやすくするために、状態を統合したり型や定数で制約を加えたりしましょう。
こちらが想像しないような矛盾する状態が起きる可能性をなるべく排除しようという話ですね。

Avoid redundant state

原則3についての節です。
冗長な状態は避けようという話ですね。
このドキュメントにも度々出てくる名前の例が挙げられています。

//BAD 状態の組み合わせで計算できる冗長な状態が存在する。
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');

//GOOD 計算することで冗長な状態が避けられている。
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');

const fullName = firstName + ' ' + lastName;

上記の例の場合はコストの掛からない処理ですが、計算量の多い処理などの場合はこの限りではなさそうです。
計算できて、コストが高くないなら状態として持っておく意味はないよね、という話ですね。

Avoid duplication in state

原則4についての節です。
状態の重複を避けようという話ですね。意外とこれやってしまうので気をつけなければと思う限りです。

//BAD 同じ情報を持った状態が複数存在している。
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );
  
//GOOD それぞれの状態で必要な異なる情報を保持している。
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(0);

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

上記の例だと選択しているかどうかを判断するのにID以外の情報はいらないので、IDだけ持ってあげて残りはレンダリング中に計算しようよという話でした。

Avoid deeply nested state

原則5についての節です。
深くネストされた状態はやめようという話ですね。
わざわざ言わなくてもわかるとは思いますが、ネストの深いオブジェクトは管理が大変です。

//BAD 冗長、かつネストが深く更新しづらい。
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: []
          }, {
              id: 5,
              title: 'Kenya',
              childPlaces: []
          }, {
              id: 6,
              title: 'Madagascar',
              childPlaces: []
          }, {
              id: 7,
              title: 'Morocco',
              childPlaces: []
          }, {
              id: 8,
              title: 'Nigeria',
              childPlaces: []
          }, {
              id: 9,
              title: 'South Africa',
              childPlaces: []
          }]
      }, {
          id: 10,
          title: 'Americas',
          childPlaces: [{
              id: 11,
              title: 'Argentina',
              childPlaces: []
          }, {
              id: 12,
              title: 'Brazil',
              childPlaces: []
          }, {
              id: 13,
              title: 'Barbados',
              childPlaces: []
          }, {
              id: 14,
              title: 'Canada',
              childPlaces: []
          }, {
              id: 15,
              title: 'Jamaica',
              childPlaces: []
          }, {
              id: 16,
              title: 'Mexico',
              childPlaces: []
          }, {
              id: 17,
              title: 'Trinidad and Tobago',
              childPlaces: []
          }, {
              id: 18,
              title: 'Venezuela',
              childPlaces: []
          }]
      }, {
          id: 43,
          title: 'Moon',
          childPlaces: [{
              id: 44,
              title: 'Rheita',
              childPlaces: []
          }, {
              id: 45,
              title: 'Piccolomini',
              childPlaces: []
          }, {
              id: 46,
              title: 'Tycho',
              childPlaces: []
          }]
      },]
  }]
};

//GOOD フラットになっていて冗長ではない。
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]
    },
};

ネストされたオブジェクトを見たときは、

  • フラットにできないか。
  • ネストされた部分を子コンポーネントに移行できないか。

などを検討しましょう。

まとめ

状態の構造化について読み進めていきました。
言われると簡単なことのように思いますが、実装しているときには意外と気づきにくいものです。
前回の記事のように冗長さや重複していないかというのをリファクタするプロセスが開発中に盛り込めていれば気づけるでしょう。
実装したい機能が正しくモデリングできているかも重要そうです。
原理原則として覚えておくべきだと感じました。

GitHubで編集を提案

Discussion