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

一部の JavaScript 関数は純粋です。純粋な関数は計算だけを行い、それ以上のことはしません。コンポーネントを純粋な関数としてのみ厳密に記述することで、コードベースが成長するにつれて、不可解なバグや予測不可能な動作のクラス全体を回避できます。ただし、これらの利点を享受するには、従う必要のあるいくつかのルールがあります。

次のことを学びます

  • 純粋性とは何か、またそれがどのようにバグの回避に役立つか
  • レンダリングフェーズから変更を排除することで、コンポーネントを純粋に保つ方法
  • コンポーネント内の間違いを見つけるために厳格モードを使用する方法

純粋性: 式としてのコンポーネント

コンピュータサイエンス (特に関数型プログラミングの世界) では、純粋な関数は、次の特性を持つ関数です。

  • 自分のことは自分でやる。呼び出される前に存在していたオブジェクトや変数を変更しません。
  • 同じ入力には、同じ出力。同じ入力が与えられた場合、純粋な関数は常に同じ結果を返す必要があります。

純粋な関数の例の 1 つとして、数学の式は既にご存知かもしれません。

次の数学の式を考えてみましょう: 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 です。

A tea recipe for x people: take x cups of water, add x spoons of tea and 0.5x spoons of spices, and 0.5x cups of milk

イラスト Rachel Lee Nabors

副作用: (意図しない) 結果

Reactのレンダリングプロセスは常に純粋でなければなりません。コンポーネントはJSXを返すだけで、レンダリング前に存在していたオブジェクトや変数を変更してはいけません。それはコンポーネントを不純にしてしまいます。

以下は、このルールを破るコンポーネントの例です。

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であると信頼できません。テストが失敗したり、ユーザーが混乱したり、飛行機が空から落ちたりする可能性があります。これが混乱を招くバグにつながる理由がわかるでしょう!

このコンポーネントは、guestをpropとして渡すことで修正できます。

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はguestプロップのみに依存するため、コンポーネントは純粋になります。

一般的に、コンポーネントが特定の順序でレンダリングされることを期待すべきではありません。y = 2xy = 5xの前または後に呼び出すかどうかは問題ではありません。両方の式は互いに独立して解決されます。同じように、各コンポーネントは「自分自身で考える」だけで、レンダリング中に他のコンポーネントと協調したり、依存したりしようとすべきではありません。レンダリングは学校の試験のようなもので、各コンポーネントはJSXを独自に計算する必要があります!

深掘り

StrictModeによる不純な計算の検出

まだすべてを使用したことがないかもしれませんが、Reactでは、レンダリング中に読み取ることができる3種類の入力があります。propsstate、そしてcontextです。これらの入力は常に読み取り専用として扱う必要があります。

ユーザー入力に応じて何かを変更したい場合は、変数を書き込むのではなくstateを設定する必要があります。コンポーネントがレンダリング中に、既存の変数やオブジェクトを変更すべきではありません。

Reactは、開発中に各コンポーネントの関数を2回呼び出す「Strict Mode」を提供します。コンポーネント関数を2回呼び出すことで、StrictModeはこれらのルールを破るコンポーネントを見つけるのに役立ちます。

元の例では、「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>でラップできます。一部のフレームワークでは、これがデフォルトで設定されています。

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

上記の例では、問題は、コンポーネントがレンダリング中に既存の変数を変更したことでした。これは、少し怖く聞こえるように「ミューテーション」と呼ばれることが多いです。純粋な関数は、関数のスコープ外の変数や、呼び出し前に作成されたオブジェクトをミューテートしません。そうすると不純になってしまいます!

しかし、レンダリング中に作成したばかりの変数やオブジェクトを変更することは、まったく問題ありません。この例では、[]配列を作成し、それをcups変数に割り当て、次に1ダースのカップをpushしています。

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

export default function TeaGathering() {
  let cups = [];
  for (let i = 1; i <= 12; i++) {
    cups.push(<Cup key={i} guest={i} />);
  }
  return cups;
}

cups変数または[]配列がTeaGathering関数の外で作成された場合、これは大きな問題になります!その配列に項目をプッシュすることで、既存のオブジェクトを変更することになります。

ただし、同じレンダリング中に、TeaGathering内で作成したため、問題ありません。TeaGatheringの外のコードは、このことが発生したことを知ることはありません。これは「ローカルミューテーション」と呼ばれます。コンポーネントの小さな秘密のようなものです。

副作用を起こすことができる場所

関数型プログラミングは純粋性に大きく依存していますが、どこかの時点で何らかの変更が必要です。それがプログラミングの目的だからです!これらの変更(画面の更新、アニメーションの開始、データの変更など)は、副作用と呼ばれます。それらは、レンダリング中ではなく、「側で」発生するものです。

Reactでは、副作用は通常、イベントハンドラの中にあります。イベントハンドラは、ボタンをクリックしたときなど、何らかのアクションを実行するとReactが実行する関数です。イベントハンドラはコンポーネントで定義されていますが、レンダリングには実行されません!したがって、イベントハンドラは純粋である必要はありません。

他のすべてのオプションを使い果たし、副作用に適したイベントハンドラが見つからない場合は、コンポーネント内のuseEffect呼び出しで、返されたJSXに付加することができます。これにより、Reactは、副作用が許可されているレンダリング後に実行するように指示されます。ただし、このアプローチは最後の手段とすべきです。

可能な限り、レンダリングだけでロジックを表現するようにしてください。これでどこまで到達できるか驚くでしょう!

深掘り

Reactが純粋性を重視するのはなぜですか?

純粋な関数を書くには、ある程度の習慣と規律が必要です。しかし、それは素晴らしい機会も切り開きます。

  • あなたのコンポーネントは、例えばサーバー上など、異なる環境で実行される可能性があります。同じ入力に対して同じ結果を返すため、1つのコンポーネントで多数のユーザーリクエストに対応できます。
  • 入力が変更されていないコンポーネントのレンダリングをスキップすることで、パフォーマンスを向上させることができます。純粋関数は常に同じ結果を返すため、キャッシュしても安全です。
  • 深いコンポーネントツリーのレンダリング中にデータが変更された場合、Reactは古くなったレンダリングを完了するのに時間を無駄にすることなく、レンダリングを再開できます。純粋であるため、いつでも計算を停止しても安全です。

私たちが構築している新しいReactのすべての機能は、純粋性を利用しています。データフェッチからアニメーション、パフォーマンスまで、コンポーネントを純粋に保つことで、Reactのパラダイムの力を引き出すことができます。

要約

  • コンポーネントは純粋である必要があります。つまり、
    • 自分のことは自分でやる。レンダリング前に存在していたオブジェクトや変数を変更してはいけません。
    • 同じ入力には、同じ出力。同じ入力が与えられた場合、コンポーネントは常に同じJSXを返す必要があります。
  • レンダリングはいつでも発生する可能性があるため、コンポーネントは相互のレンダリング順序に依存してはいけません。
  • コンポーネントがレンダリングに使用する入力を変更してはいけません。これには、props、state、contextが含まれます。画面を更新するには、既存のオブジェクトを変更するのではなく、stateを「設定」します。
  • コンポーネントのロジックは、返すJSXで表現するように努めてください。「何かを変更する」必要がある場合は、通常、イベントハンドラーで行います。最後の手段として、useEffectを使用できます。
  • 純粋関数を書くには少し練習が必要ですが、Reactのパラダイムの力を引き出すことができます。

課題 1 3:
壊れた時計を修正する

このコンポーネントは、午前0時から午前6時までの間、<h1>のCSSクラスを"night"に、その他の時間は"day"に設定しようとしています。ただし、動作しません。このコンポーネントを修正できますか?

一時的にコンピュータのタイムゾーンを変更することで、ソリューションが機能するかどうかを確認できます。現在の時刻が午前0時から午前6時の間である場合、時計の色が反転しているはずです!

export default function Clock({ time }) {
  let hours = time.getHours();
  if (hours >= 0 && hours <= 6) {
    document.getElementById('time').className = 'night';
  } else {
    document.getElementById('time').className = 'day';
  }
  return (
    <h1 id="time">
      {time.toLocaleTimeString()}
    </h1>
  );
}