ReactはuseEffectで副作用があるのに、なぜ純関数と言えるのか?
Reactは関数型の思想で UI = f(状態) を実現している一方で、useEffect のように副作用を扱う仕組みも提供しています。
純関数を前提とした設計なのに、副作用を持ち込んでなぜ成り立つのかが気になったため、整理してみました。
useEffectって何?
分解するとuse + Effect = エフェクトを扱うという意味になると思います。
じゃあ、エフェクトって何かというと
エフェクトは、特定のイベントによってではなく、レンダー自体によって引き起こされる副作用を指定するためのものです。
と記載があります。
例えば、チャットでのメッセージ送信は、ユーザが特定のボタンをクリックすることによって直接引き起こされるため、クリックという特定のイベントです。
しかし、サーバ接続のセットアップは、コンポーネントが表示される原因となるインタラクションに関係なくレンダー自体によって行われるべきであるため、エフェクトです。
よって、useEffectはコンポーネントのレンダー自体によって引き起こされる副作用を扱うフックとなります。
実行フロー
次に、useEffectがどのようなフローで実行されているか考えてみようと思います。
基本的なフローとしてはコンポーネントが
レンダー → コミット → ペイント
まで終わったタイミングでuseEffectが実行されることになります。
useEffectはブラウザのペイント後に実行されるため、描画をブロックせずに実行できることがわかります。
※ 多くの場合 useEffect はペイント後に実行されますが、ユーザー操作由来の更新などは、ペイント前に実行されることもあります。
※useLayoutEffectを使用するとペイント前に実行することができますが、パフォーマンスとトレードオフになります。
※初回マウント時はクリーンアップ関数は実行されません。

副作用があっても純関数が成り立つのか
ここで、本題の副作用を扱うuseEffectなどがあっても純関数として問題ないのか確認していきたいと思います。
そもそも、純関数とは何かというと下記のことを指しております。
コンピュータサイエンス(特に関数型プログラミングの世界)では、純関数 (pure function) とは、以下のような特徴を持つ関数のことを指します。
- 自分の仕事に集中する。
呼び出される前に存在していたオブジェクトや変数を変更しない- 同じ入力には同じ出力。
同じ入力を与えると、純関数は常に同じ結果を返す。
Reactでは、UIをブラウザに描画する必要があるため、 アプリケーション全体を純関数だけで構成することはできません。
じゃあどの部分が純粋であるのかというと、Reactではレンダー処理が純粋であることを前提として設計されています。
これは、先ほど説明した純関数の定義を、React のレンダー処理に当てはめると次のようになります。
-
自分の仕事に集中する。
純粋であるためにはレンダー前に存在していたオブジェクトや変数を書き換えないようにしなければなりません。 -
同じ入力には同じ出力。
与えられた入力(propsやstate)が同じであれば、常に同じ JSX を返す必要があります。
useEffect は、レンダー後のコミット・ペイントが完了した後に実行されるため、レンダーの純粋性を壊すことなく副作用を扱うことができます。
同様に、クリックイベントなどのイベントハンドラも、レンダー時に定義されたものではありますが、レンダー時に実行されるわけではなく、ユーザー操作によって明示的に呼び出される関数のため純粋である必要はありません。
レンダー処理を純粋に保つ理由
レンダー処理を純粋に保つ理由は、 React がレンダーを何度でも再実行・中断・破棄できるようにするためになります。
レンダーが純粋であれば、React は更新の優先度に応じて、重要度の低いコンポーネントのレンダーを一時停止し、後で再開したり、不要になったレンダー結果を破棄したりできます。
一方、副作用はこの前提を壊します。
レンダー中に副作用があると、React がレンダーを再実行・破棄した際に、意図しないタイミングで副作用が発生してしまうためです。
そのため副作用をレンダーフェーズから隔離し、UI が確定した後のフェーズでのみ実行されるように設計されています。
図で表すと下記のようなイメージになるかと思います

つまり、React は副作用を禁止しているのではなく、副作用が安全に実行できる場所を明確に分離することで関数型の思想を保つことができています。
参考
useEffect – React
エフェクトを使って同期を行う – React
コンポーネントを純粋に保つ – React
コンポーネントとフックを純粋に保つ – React
Discussion
副作用があればその時点で純関数とは言えません
HaskellはIOモナドで副作用があるのになぜ純粋関数型言語と言えるのかって言ってるようなもの