render hooks パターンの注意点と対策
以下の記事で紹介されている「render hooks パターン」には注意点があるため、簡単な例と共に注意点についてまとめました。
要約
Custom Hook から Component を返却する render hooks パターンの場合、返却された Component がアンマウントされてしまうことによりバグが発生する恐れがあるので注意が必要です。
Custom Hook から Component を返さずに React element を返す実装にすれば、 アンマウントを回避できます。
Component を返却する render hooks パターンの実装例と問題点
問題の視覚的確認
以下の codesandbox の例の「Open Modal」ボタンを押下してみてください。
Modal コンポーネントが現れるのですが、一定間隔でチカチカしています。
実装例
実装は以下の通りです。useModal
で render hooks パターンを使っています。 useModal
は、モーダルを開くかどうかの boolean の state (isOpen
)と、その state を変更する関数 (onClose
) を Modal
コンポーネントに取り込んだ、ModalFromHook
コンポーネントを作成し、返却しています。
useModal
を利用するコンポーネントは、モーダルが開いているかどうかの状態(isOpen
)を意識しなくてよく、 モーダルをオープンしたい時にonOpen
を呼べばモーダルをオープンできます。状態が隠蔽されています。
問題の内容
しかし、上記のようにmemo化なしに Custom Hook (useModal
)内でコンポーネント(ModalFromHook
)を作成した場合、その Custom Hook (useModal
)が呼び出されるされる毎(※)に、作成されたコンポーネント(ModalFromHook
)はアンマウントされてしまいます。
(※ 後述の通り、useCallbackでmemo化した場合は状態(isOpen
)が変更される毎にアンマウントされます。)
実装例では視覚的に分かりやすくするために、Custom Hook (useModal
)を利用しているコンポーネントを、1秒毎に強制的にレンダリングして、アンマウントされる現象を意図的に発生させています。また、Custom Hook useModal
をレンダーする毎に"render"を、Modal
コンポーネントがアンマウントされる毎に"unmount"を表示するようにしています。上記 codesandbox の console を見ると、1秒毎に"render"の直後に"unmount"が表示されていることから、Custom Hook (useModal
)のレンダリング毎にアンマウントが発生していることが確認できます。
メモ化してもアンマウントされる
React.memo を利用した場合
このアンマウントされる現象は、Custom Hook (useModal
)内で作成されるコンポーネント(ModalFromHook
)をmemo化しても発生します。
React.useCallback を利用した場合
useCallback
を用いた場合、Custom Hook (useModal
)が呼び出される毎にアンマウントされることはなくなります。しかし、状態(isOpen
)が変更される毎(モーダルを開いたり閉じたりする毎)にアンマウントされることが、 console にて "unmount" が表示されることから確認できます。
(memoとのdiff)
問題の考察
もし、Custom Hook から返却されるコンポーネントが、内部の状態を重視するコンポーネントである場合、アンマウントにより状態がリセットされるのは不都合です。さらに、マウントした時だけ実行するコード(データのフェッチなど)をコンポーネントに書いた場合、アンマウントが発生すると意図せずそのコードが実行される結果になりかねません。最初は簡単な実装だからアンマウントされても良いと考え、Component を返すrender hooks パターンを許容しても、後々複雑になり、問題を発生させる可能性もあります。 この時、デバッグを難しく感じるエンジニアもいるかもしれません。
React element を返却する render hooks パターン
アンマウントは発生しない
React elements を返却する render hooks パターンでは、アンマウントされる現象は発生しません。
下記の codesandbox で、 Console には "render" と繰り返し表示されていますが、"unmount" は表示されなくなっています。また、モーダルを開いてもチカチカしないことからもアンマウントされていないことが分かります。
(コードのdiff)
React element には props を渡せない問題について
Component とは違い、React element には props を渡すことができないので、呼び出す側(ModalButton
)でモーダルコンポーネントの props を調整できません。呼び出す側(ModalButton
)でモーダルコンポーネント(Modal
)の props を調整するため、以下の実装例では、Custom Hook (useModal
)にモーダルコンポーネント(Modal
)の props を渡す実装にしています。
感想
state を UI に閉じ込め、かつ、state の変更方法を制約する render hooks パターンの方向性は良いと思います。React は Custom Hook 内で Component を作成することを考慮して作られていないようなので、そのような実装は自分は避けたいなと思いますが、 element を返す render hooks パターンであれば採用はありだと思いました。Custom Hooks に Component の props を渡すのは少し違和感がありますが、違和感程度の問題と思います。また、呼び出し側で props を調整する必要がない場合、実装例のように Custom Hook に Component の props を渡す必要もありません。
Component を Custom Hook から返却する render hooks パターンでは、 Component の props の一部を(stateで)固定して新しい Component を作成しています。これは関数型プログラミングでいうところの部分適用だと思ってます。そうだとするとこの記事で指摘している注意点を別の表現にすると、「Custom Hook 内で Component を動的に部分適用するのは要注意」と表現できます。ところで、静的に Component に部分適用することは可能なようでして、それに関してはこちらに記事を書いているので興味があれば読んでいただければと思います。
Discussion
有益な記事の執筆ありがとうございます🙂
上記に関してなのですが、memo化が効かなかった理由は
useDisclosure
の返値が再生成されているからではないでしょうか?例えば、以下のような形式であればmemo化は機能するかと思われます
こちらこそ記事を読んでいただき、また有益なご指摘をくださり、本当にありがとうございます。
ご指摘の点について、記事とソースコードを修正しましたので、ご確認いただけますと幸いです。