💡

コンポーネントとフックを純粋に保つ

に公開

まとめ

  • レンダー時を純粋に保つ

  • コンポーネントは冪等

  • 副作用をレンダー時から切り離す

  • ユーザーからのイベントの副作用はイベントハンドラ、レンダー直後に動作させたい場合はエフェクト

  • ローカルミューテーション。ローカル変数は書き換えていい

  • propsはイミュータブル。ローカル変数として定義する

  • stateはイミュータブル。セッタ関数を使って更新する

  • フックの引数と返り値はイミュータブル

  • JSXの返り値に渡された値はイミュータブル

  • MVCのMの部分。ロジックとデータを分離している。Vはコンポーネント指向でファイルにまとめられている。

  • Model

    • データ
      • 固定データ (= イミュータブルなデータ)
      • 変化するデータ (= state)
    • ロジック
      • 純粋な関数
      • 副作用のある関数
        I
  • イミュータブルという言葉はわかりにくいので読み取り専用と訳した方がいいのか?readonlyとimmutableは違いあるのか?

純粋な関数とは

関数型プログラミングにおける純粋関数とは、引数以外に入力がなく、返り値違いに出力がない関数のことです。Reactではコンポーネントやフックを純粋に保とうと言っています。コンポーネント全体を純粋に保とうというよりは、レンダーやフックに純粋性があると考えたほうがいいかもしれません。

冪等であること

同じ入力で実行するたびに常に同じ結果が得られること。レンダー中に実行されるあらゆるコードは冪等である必要があります。コンポーネントの入力とはpropsstateとコンテクスト。フックの入力とはその引数のことです。

レンダー時に副作用がない

関数型プログログラミングでは関数に副作用がないことを言いますが、UIを実装するときに完全に副作用をなくすことは現実的ではありません。そこでReactでは少し条件を緩めて、レンダー時に副作用がないことを要請しています。Reactは複数のフェーズに分けてコードを実行しますが、レンダー時においては副作用がないようにし、副作用は別のフェーズで行うようにします。

ユーザーからのイベントによって生じる副作用はイベントハンドラとして定義します。
レンダー直後に動作させたい場合はエフェクトとして実行します。

ローカルな値以外を変更しない

コンポーネントとフックは、レンダー中にローカルに作成されたものではない値を変更してはいけません。

コンポーネントとフックを冪等にする

コンポーネントは、その入力であるpropsstate、およびコンテクストに対して常に同じ出力を返さなければなりません。これを冪等性と呼びます。これは、レンダー中に実行されるあらゆるコードは冪等でなければならないという意味です。

値の書き換えを行なっても良いタイミング

ローカルミューテーション

副作用の一般的な例はミューテーションです。JavaScriptでは非プリミティブ型の値(配列やオブジェクトなど)の内容を書き換えることを指します。一般的にReactでは変数の書き換えは避けるべきですが、ローカル変数のミューテーションは全く問題ではありません。

function FriendList({ friends }) {
  const items = []; // ✅ ローカル変数として定義
  for (let i = 0; i < friends.length; i++) {
    const friend = friends[i];
    items.push(
      <Friend key={friend.id} friend={friend} />
    ); // ✅ push()でitemsの内容を書き換えているが、ローカルミューテーションは問題なし
  }
  return <section>{items}</section>;
}

一方で、itemsがコンポーネントの外側で作成されている場合、以前の値を保持し続けることで、変更の記憶が起きてしまう。FriendListコンポーネントが再度レンダーされると、friendsitemsに追加し続けられる。つまり、コンポーネントのレンダーのたびに異なるJSXを返してしまうので冪等ではなくなってしまう。
コンポーネント内部で外部状態を変更することを副作用と呼ぶ。今回は、コンポーネント外部で定義したitemsを変更しようとしたため副作用が生じ、その結果としてコンポーネントが純粋でなくなってしまう。

ちなみにstateはコンポーネントに対してローカルな値なので、セット関数による更新も実質ローカル変数を書き換えてるローカルミューテーション。

https://ja.react.dev/learn/state-a-components-memory#state-is-isolated-and-private

遅延初期化

厳密には「純粋」ではありませんが、遅延初期化は問題ありません

function ExpenseForm() {
  SuperCalculator.initializeIfNotReady(); // ✅ Good: if it doesn't affect other components
  // Continue rendering...
}

propsstateはイミュータブル

コンポーネントのpropsstateはスナップショットであり、イミュータブルです。これらは決して直接書き換えてはいけません。代わりに、新しいpropsを渡すか、useStateのセッタ関数を使用してください。

これもまた副作用を起こさないため、という上と同じ理由。propsstateを書き換えると、コンポーネントの外側にあるデータを変更してしまい副作用が発生する。

スナップショットという単語が出てきたが、これは公式ドキュメントでも出てくる用語であり、「stateはスナップショットである」という記事もある。下は参考記事。
https://qiita.com/honey32/items/ee8d1577e68b0d58678d

propsを書き換えない

propsを書き換えたい」と思う状況になった場合、コンポーネント内でコピーして、ローカル変数として定義。先ほど解説したようにコンポーネント内ぶのローカル変数は書き換えてもいい(ローカルミューテーション)。

function Post({ item }) {
  item.url = new Url(item.url, base); // 🔴 Bad: props に再代入してはいけない
  return <Link url={item.url}>{item.title}</Link>;
}
function Post({ item }) {
  const url = new Url(item.url, base); // ✅ Good: propsのコピーとしてローカル変数を定義する
  return <Link url={url}>{item.title}</Link>;
}

stateを書き換えない

stateuseStateで受け取るセッタ関数を使って値を書き換える。state変数の中身を書き換えてもコンポーネントが更新されるわけではない。セッタ関数を使用することで、stateが変更され、UIを更新するために再レンダーする必要があることをReactに伝える

function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    count = count + 1; // 🔴 Bad: state を直接書き換えてはいけない
  }

  return (
    <button onClick={handleClick}>
      You pressed me {count} times
    </button>
  );
}
function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1); // ✅ Good: state はセッタ関数を使って書き換える
  }

  return (
    <button onClick={handleClick}>
      You pressed me {count} times
    </button>
  );
}

フックの引数と返り値はイミュータブル

カスタムフックの話をしている。今の段階では自分のレベルにあってないのでまた書き足す

https://ja.react.dev/reference/rules/components-and-hooks-must-be-pure#return-values-and-arguments-to-hooks-are-immutable

JSX に渡された値はイミュータブル

JSXで使用された値を書き換えてはいけません。値の書き換えはJSXが作成される前に行う。

式としてJSXを使用する際、Reactはコンポーネントのレンダーが完了する前にJSXを先行して評価してしまうかもしれません。つまりJSXに渡された後で値を変更した場合、Reactがコンポーネントの出力を更新する必要があることを認識しないため、古いUIが表示され続ける可能性がある。

function Page({ colour }) {
  const styles = { colour, size: "large" };
  const header = <Header styles={styles} />;
  styles.size = "small"; // 🔴 Bad: コンポーネントにスタイルを代入した後、変更してはいけない。
  const footer = <Footer styles={styles} />;
  return (
    <>
      {header}
      <Content />
      {footer}
    </>
  );
}
function Page({ colour }) {
  const headerStyles = { colour, size: "large" };
  const header = <Header styles={headerStyles} />;
  const footerStyles = { colour, size: "small" }; // ✅ Good: ローカル変数を作成して変更し、コンポーネントに代入
  const footer = <Footer styles={footerStyles} />;
  return (
    <>
      {header}
      <Content />
      {footer}
    </>
  );
}

Discussion