freee会計の React 17 化を進めていたら flux に PR を出していた話

この記事は freee Developers Advent Calendar 2021 の 21 日目の記事です.

freee でエンジニアをやっているけむりだま (@_kemuridama) です. freee会計の開発をメインに freee Tech Night の運営リーダーをやっていたり, オンラインになった社内イベントの企画や配信をやっていたりします.

今回は freee会計の React 17 化を進めていた際に facebook/flux のバグにハマって, 修正のために PR を出してパッチリリースをしてもらった話をしようと思います. 2018 年に新卒で入社してから毎年 Advent Calendar に参加してますが 3 年ぶり 2 回目の技術的な内容になりますw

経緯

freee会計を React 16 から 17 にアップデートするために, アップデート起因でサービスの動作のデグレが発生していないか QA を行ってもらっていました. その中でとあるページの一覧画面にある検索フォームが動かないというチケットが起票されました.

細かい動作を調べてみると検索ボタンを押した際に query string の変更は入るが再検索が何故か走らずに一覧画面のデータが更新されないという問題でした. React 17 には基本的に新機能はなく大きな破壊的変更もイベントデリゲーションに関するものだけで, 今回のようにボタンのクリックイベントをハンドリングして query string を変更するという処理は影響を受けないように見えました.

Developer Tools を使ってデバッグをしながら詳細に動作を追っていると UNSAFE_componenWillReceiveProps() が呼び出されていないということが発覚しました. これは React の class component に存在している lifecycle method というもので, 親から渡される props が更新されると呼び出されるメソッドです.

今回のページは以下のような動きで検索機能を実現してました.

  1. 検索ボタンが押されて history.push() で query string を更新
  2. React Router が変更された query string を元に component に渡す props を更新
  3. UNSAFE_componentWillReceiveProps() で props の更新を検知して検索を実行
  4. 検索結果を元に一覧画面を更新

しかし 3 の UNSAFE_componentWillReceiveProps() が実行されないので props の更新を検知できず検索が実行されないため期待した動作をしなくなってしまっていました.

UNSAFE_ という prefix がつく lifecycle method は将来的に削除される予定であることは知っていましたが, 16 から 17 へのアップデートでそれが動かなくなったのは想定外でした. 試しに他の UNSAFE_ という prefix がつく lifecycle method も動いていないのか確認しましたが正しく呼び出されているように見えました.

どうやら UNSAFE_componentWillReceiveProps() だけが呼ばれていないようだ…

React のコードを見てみる

この時点では React のアップデートがこの問題の原因だと思っていたので, React のコードを読んでみることにしました. React 17 で UNSAFE_componentWillReceiveProps() の呼び出しが記述されているのはこの部分だ.

https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberClassComponent.new.js#L775-L805

コードを読むと callComponentWillReceiveProps() は props が変更されると呼び出され, class component に定義された componentWillReceiveProps()UNSAFE_componentWillReceiveProps()function であればそれらを呼び出すようでした.

componentWillReceiveProps()componentWillMount(), componentWillUpdate() と並んで既に deprecated になっていて, v16.3 で導入された UNSAFE_ prefix が付いたものを使うことになっています. (UNSAFE_ prefix が付いたものもレガシーと位置づけられていて新規コードで使うのは避けるべきです.) しかし React 17.0.2 時点ではまだ prefix なしの lifecycle method も使えるようになっているようです.

Developer Tools を用いて callComponentWillReceiveProps() 内に breakpoint を置いて debug してみると, 該当の component に定義されている UNSAFE_componentWillReceiveProps()function となり期待通り呼び出されているようにみえます.

呼び出されているはずなのになぜか期待通りに動かない…

flux のコードを見てみる

試しに UNSAFE_componentWillReceiveProps()componentWillReceiveProps() に書き換えてみると期待通りの挙動をするようになりました. ということは自分で定義した UNSAFE_componentWillReceiveProps() がどこかで書き換えられているのではないかと推察しました.

実は問題が起きている component は container component で export する際に Container.create() で class component を container 化していました. これは facebook/flux で定義されているメソッドで, 渡された class component を継承して flux で必要なメソッドを override しています.

debug を進めていると flux による class component の継承の中で UNSAFE_componentWillReceiveProps() も override されているようだったので, flux のコードを見てみることにしました.

https://github.com/facebook/flux/blob/4.0.0/src/container/FluxContainer.js#L151-L163

flux によって継承された class component の UNSAFE_componentWillReceiveProps() では super.componentWillReceiveProps() が存在する場合はそれを呼び出すという記述だけで, override されてしまった super.UNSAFE_coponentWillReceiveProps() は呼び出されないということが分かります.

何ということだ…😇

なぜこの問題が起きたのか

今回 React 17 化をするに当たって peerDependencies に React 17 の追加が行われている flux も v3.1.3 から v4.0.0 にアップデートしていました. v3.1.3 時点 では継承された class component は componentWillReceiveProps() を override して super.componentWillReceiveProps() が存在していたらそれを呼び出すという実装になっています. そのため UNSAFE_componentWillReceiveProps() は flux によって継承されることなく, そのまま React の lifecycle 通りに呼び出されて期待通りの挙動をすることが分かります. freee会計内の class component の deprecated な lifecycle はすべて UNSAFE_ prefix 付きのものに置換されているのでアップデート前の v3.1.3 では期待通りの挙動をしていることが証明されました.

まとめると…

flux v3.1.3

  • componentWillReceiveProps() が flux によって override され, super.componentWillReceiveProps() が存在すればそれを呼び出す
  • super.UNSAFE_componentWillReceiveProps() は flux によって override されないので React の lifecycle 内で普通に呼び出される
  • freee会計内の componentWillReceiveProps() はすべて UNSAFE_componentWillReceiveProps() に置換されているが flux で継承された際に影響を受けないので期待通り動作をする

flux v4.0.0

  • UNSAFE_componentWillReciveProps() が flux によって override され, super.componentWillReceiveProps() が存在すればそれを呼び出す
  • super.UNSAFE_componentWillReceiveProps() は flux によって override されてしまっている, かつ override したメソッドの内部でも呼び出されないので一生呼び出されない
  • freee会計内の componentWillReceiveProps() はすべて UNSAFE_componentWillReceiveProps() に置換されていて flux で継承された際に影響を受けるので期待通り動作しなくなる

flux v4.0.0 で override したメソッドと内部で呼び出す super class のメソッドが食い違ってしまったことによって起きたバグということになります.

PR を出してパッチリリースをしてもらう

解決するのは簡単で UNSAFE_componentWillReceiveProps() を override した際に super.componentWillReceiveProps() だけではなく super.UNSAFE_componentWillReceiveProps() も呼び出せば良いです. 期待通りの挙動をするように flux に PR を出しました.

github.com

flux は既にメンテナンスモードになっていますが, 無事にメンテナーによって merge されて patch release も出してもらえました, ありがたい 🙏 flux から patch release がでなければ社内で fork して patch を当ててそれを使うというところまで考えていたので本当に良かったです.

まとめ

今回は freee会計が React 17 化するに当たって直面した flux のバグとその調査と解決の話をしました. 自分は普段からフロントエンドのライブラリアップデートや技術的負債の解消などをよくやっているのですが, OSS に対して contribution をするところまでやったのはあまりなかったのでとても良い経験でした. 今後もこのような取り組みを続けていけたらなと思います.

明日は yuichiro がスクラム採用について書いてくれるみたいです, お楽しみに!!