コンポーネントを純粋に保つ

JavaScript 関数の中には、純関数 (pure function) と呼ばれるものがあります。純関数とは計算だけを行い、他には何もしない関数のことです。コンポーネントを常に厳密に純関数として書くことで、コードベースが成長するにつれて起きがちな、あらゆる種類の不可解なバグ、予測不可能な挙動を回避することができます。ただし、このようなメリットを得るためには、従わなければならないルールがいくつか存在します。

このページで学ぶこと

  • 「純粋」であるとは何か、それによりなぜバグが減らせるのか
  • 変更をレンダーの外で行い、コンポーネントを純粋に保つ方法
  • Strict Mode を使用してコンポーネントの間違いを見つける方法

純粋性:コンポーネントとは数式のようなもの

コンピュータサイエンス(特に関数型プログラミングの世界)では、純関数 (pure function) とは、以下のような特徴を持つ関数のことを指します。

  • 自分の仕事に集中する。呼び出される前に存在していたオブジェクトや変数を変更しない。
  • 同じ入力には同じ出力。同じ入力を与えると、純関数は常に同じ結果を返す。

皆さんは純関数の例をひとつ、すでにご存知のはずです。数学における関数です。

この数式を考えてみてください:y = 2x

もし x = 2 ならば y = 4。常にです。

もし x = 3 ならば y = 6。常にです。

もし x = 3 ならば、y が現在時刻や株式市況に影響されてたまに 9–12.5 になったりはしません。

もし y = 2x かつ x = 3 なら、yどんな場合でも常に 6 になるのです。

この式を JavaScript 関数で書くとすると、次のようになります:

function double(number) {
return 2 * number;
}

上記の例では、double 関数は純関数です。もし 3 を渡すと、6 を返しますね。常にです。

React はこのような概念に基づいて設計されています。React は、あなたが書くすべてのコンポーネントが純関数であると仮定しています。つまり、あなたが書く React コンポーネントは、与えられた入力が同じであれば、常に同じ JSX を返す必要があります。

function Recipe({ drinkers }) {
  return (
    <ol>
      <li>Boil {drinkers} cups of water.</li>
      <li>Add {drinkers} spoons of tea and {0.5 * drinkers} spoons of spice.</li>
      <li>Add {0.5 * drinkers} cups of milk to boil and sugar to taste.</li>
    </ol>
  );
}

export default function App() {
  return (
    <section>
      <h1>Spiced Chai Recipe</h1>
      <h2>For two</h2>
      <Recipe drinkers={2} />
      <h2>For a gathering</h2>
      <Recipe drinkers={4} />
    </section>
  );
}

drinkers={2}Recipe に渡すと、2 cups of water を含む JSX が返されます。常にです。

drinkers={4} を渡すと、4 cups of water を含む JSX が返されます。常にです。

そう、まるで数式のように、です。

コンポーネントとはレシピのようなものだと考えることもできるでしょう。調理途中で新しい食材を加えたりせず、レシピに従っておけば、常に同じ料理を得ることができます。その「料理」とは、コンポーネントが React に提供する JSX のことであり、それを React が表示します。

人 x 人分のお茶のレシピ:x cups of water を取り、x spoons of tea と0.5x spoons of spices を加え、0.5x cups of milk を入れる

Illustrated by Rachel Lee Nabors

副作用:意図せぬ (?) 付随処理

React のレンダープロセスは常に純粋である必要があります。コンポーネントは JSX を返すだけであり、レンダー前に存在していたオブジェクトや変数を書き換えしないようにしなければなりません。さもなくばコンポーネントは不純 (impure) になってしまいます!

以下は、この規則を守っていないコンポーネントの例です。

let guest = 0;

function Cup() {
  // Bad: changing a preexisting variable!
  guest = guest + 1;
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup />
      <Cup />
      <Cup />
    </>
  );
}

このコンポーネントは、外部で宣言された guest 変数を読み書きしています。つまり、このコンポーネントを複数回呼び出すと、異なる JSX が生成されます! さらに悪いことに、ほかのコンポーネントも guest を読み取る場合、それらもレンダーされたタイミングによって異なる JSX を生成することになります! これでは予測不可能です。

数式 y = 2x の例に戻ると、これは x = 2 であっても y = 4 であることが保証されない、というようなことです。テストは失敗し、ユーザは当惑し、飛行機も空から墜落しかねません。こんなことをするとなぜ混乱するバグが引き起こされるのか、もうおわかりですね。

props を使って guest を渡すように、このコンポーネントを修正できます。

function Cup({ guest }) {
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup guest={1} />
      <Cup guest={2} />
      <Cup guest={3} />
    </>
  );
}

これでこのコンポーネントは純粋になります。返す JSX が props である guest のみに依存しているからです。

一般に、特定の順序でコンポーネントがレンダーされることを期待してはいけません。y = 2xy = 5x のどちらを先に呼ぶかなど問題にしてはいけないのです。これらの数式は互いに無関係に計算されるべきです。同じように、各コンポーネントは「自分のことだけを考える」べきであり、レンダーの最中に他のコンポーネントに依存したり他のコンポーネントと協調したりすることはありません。レンダーとは学校の試験のようなものです。各コンポーネントはそれぞれ、自分の力だけで JSX を計算する必要があるのです!

さらに深く知る

StrictMode で純粋でない計算を検出

まだ全部を使ったことはないかもしれませんが、React には propsstate、そしてコンテクストという、レンダー中に読み取ることができる 3 種類の入力値があります。これらの入力値は、常に読み取り専用として扱うようにしてください。

ユーザ入力に応じて何かを変更したい場合は、変数に書き込む代わりに、state を設定することが適切です。要素のレンダー中に既存の変数やオブジェクトを書き換えることは絶対にやってはいけません。

React には “Strict Mode” という機能があり、開発中には各コンポーネント関数を 2 回呼び出します。関数呼び出しを 2 回行うことで、Strict Mode はこれらのルールに反するコンポーネントを見つけるのに役立ちます。

元の例では “Guest #1”、“Guest #2”、“Guest #3” と表示される代わりに “Guest #2”、“Guest #4”、“Guest #6” と表示されてしまっていましたね。元の関数が純粋でなかったため、2 回呼び出すと壊れていたわけです。修正された純粋なバージョンは、毎回 2 回呼び出されても問題ありません。純関数は計算をするだけなので、2 回呼び出しても何も変わりませんdouble(2) を 2 回呼び出しても返り値が変わることはなく、y = 2x を 2 回解いても y が変わることがないのと全く同じです。入力が同じならば、出力も同じにしてください。常にそうしてください。

Strict Mode は本番環境では影響を与えないため、ユーザが使うアプリを遅くすることはありません。Strict Mode を有効にするには、ルートコンポーネントを <React.StrictMode> でラップします。一部のフレームワークでは、これがデフォルトで行われます。

ローカルミューテーション:コンポーネントの小さな秘密

上記の例では、問題はコンポーネントがレンダーの最中に既存の変数を変更していた点にありました。このような変更は、少し恐ろしい言い方では “ミューテーション(変異; mutation)” と呼ばれます。純関数は、関数のスコープ外の変数や、呼び出し前に作成されたオブジェクトをミューテートしません。そういうことをしてしまった関数は「不純」になってしまいます!

しかし、レンダー中にその場で作成した変数やオブジェクトであれば、書き換えることは全く問題ありません。この例では、