コンポーネントを純粋に保つ
JavaScript 関数の中には、純関数 (pure function) と呼ばれるものがあります。純関数とは計算だけを行い、他には何もしない関数のことです。コンポーネントを常に厳密に純関数として書くことで、コードベースが成長するにつれて起きがちな、あらゆる種類の不可解なバグ、予測不可能な挙動を回避することができます。ただし、このようなメリットを得るためには、従わなければならないルールがいくつか存在します。
このページで学ぶこと
- 「純粋」であるとは何か、それによりなぜバグが減らせるのか
- 変更をレンダーの外で行い、コンポーネントを純粋に保つ方法
- Strict Mode を使用してコンポーネントの間違いを見つける方法
純粋性:コンポーネントとは数式のようなもの
コンピュータサイエンス(特に関数型プログラミングの世界)では、純関数 (pure function) とは、以下のような特徴を持つ関数のことを指します。
- 自分の仕事に集中する。呼び出される前に存在していたオブジェクトや変数を変更しない。
- 同じ入力には同じ出力。同じ入力を与えると、純関数は常に同じ結果を返す。
皆さんは純関数の例をひとつ、すでにご存知のはずです。数学における関数です。
この数式を考えてみてください:y = 2x。
もし x = 2 ならば y = 4。常にです。
もし x = 3 ならば y = 6。常にです。
もし x = 3 ならば、y が現在時刻や株式市況に影響されてたまに 9 や –1 や 2.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 が表示します。
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 が guest
プロパティのみに依存しているからです。
一般に、特定の順序でコンポーネントがレンダーされることを期待してはいけません。y = 2x と y = 5x のどちらを先に呼ぶかなど問題にしてはいけないのです。これらの数式は互いに無関係に計算されるべきです。同じように、各コンポーネントは「自分のことだけを考える」べきであり、レンダーの最中に他のコンポーネントに依存したり他のコンポーネントと協調したりすることはありません。レンダーとは学校の試験のようなものです。各コンポーネントはそれぞれ、自分の力だけで JSX を計算する必要があるのです!
さらに深く知る
まだ全部を使ったことはないかもしれませんが、React には props、state、そしてコンテクストという、レンダー中に読み取ることができる 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)” と呼ばれます。純関数は、関数のスコープ外の変数や、呼び出し前に作成されたオブジェクトをミューテートしません。そういうことをしてしまった関数は「不純」になってしまいます!
しかし、レンダー中にその場で作成した変数やオブジェクトであれば、書き換えることは全く問題ありません。この例では、[]
配列を作成して cups
変数に代入し、それに 12 個のカップを 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
関数の外で作成されたものだった場合、これは大きな問題になることでしょう! 既存のオブジェクトを変更してしまうことになるからです。
しかし、cups
変数と []
配列は、TeaGathering
内で同一のレンダー中に作成されたものであるため、問題はありません。TeaGathering
以外のコードは、これが起こったことすら知るすべがありません。これは “ローカルミューテーション (local mutation)” と呼ばれます。あなたのコンポーネント内のちょっとした秘密のようなものです。
副作用を引き起こせる場所
関数型プログラミングには純粋性が重要であるとはいえ、いつか、どこかの場所で、何らかのものが変化しなければなりません。むしろそれがプログラミングをする意味というものでしょう。これらの変化(スクリーンの更新、アニメーションの開始、データの変更など)は 副作用 (side effect) と呼ばれます。レンダーの最中には発生しない、「付随的」なものです。
React では、副作用は通常、イベントハンドラの中に属します。イベントハンドラは、ボタンがクリックされたといった何らかのアクションが実行されたときに React が実行する関数です。イベントハンドラは、コンポーネントの「内側」で定義されているものではありますが、レンダーの「最中」に実行されるわけではありません! つまり、イベントハンドラは純粋である必要はありません。
いろいろ探してもあなたの副作用を書くのに適切なイベントハンドラがどうしても見つからない場合は、コンポーネントから返された JSX に useEffect
呼び出しを付加することで副作用を付随させることも可能です。これにより React に、その関数をレンダーの後(その時点なら副作用が許されます)で呼ぶように指示できます。ただしこれは最終手段であるべきです。
可能な限り、ロジックをレンダーのみで表現してみてください。これだけでどれだけのことができるのか、驚くことでしょう!
さらに深く知る
純関数を書くことには、多少の習慣化と訓練が必要です。しかし、それは素晴らしいチャンスをもたらすものでもあります。
- コンポーネントが異なる環境、例えばサーバ上でも実行できるようになります! 入力値が同じなら同じ結果を返すので、ひとつのコンポーネントが多数のユーザリクエストを処理できます。
- 入力値が変化しない場合、レンダーをスキップすることでパフォーマンスを向上できます。これが問題ないのは、純関数は常に同じ出力を返すため安全にキャッシュできるからです。
- 深いコンポーネントツリーのレンダーの途中でデータが変化した場合、React は既に古くなったレンダー処理を最後まで終わらせるような無駄を省き、新しいレンダーを開始できます。純粋性のおかげで、いつ計算を中断しても問題ありません。
我々が開発する React の新たな機能は常に、関数の純粋性を活用しています。データ取得からアニメーション、パフォーマンスの向上に到るまで、React パラダイムの威力はコンポーネントを純関数に保つことによって発揮されるのです。
まとめ
- コンポーネントは純粋である必要がある。すなわち:
- コンポーネントは自分の仕事に集中する。レンダー前に存在していたオブジェクトや変数を書き換えない。
- 入力が同じなら出力も同じ。同じ入力に対しては、常に同じ JSX を返すようにする。
- レンダーはいつでも起こる可能性があるため、コンポーネントは相互の呼び出し順に依存してはいけない。
- コンポーネントがレンダーに使用する入力値を書き換えない。これには props、state、コンテクストも含まれる。画面を更新するためには既存のオブジェクトを書き換えるのではなく、代わりに 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> ); }