kikitori Tech Blog

株式会社kikitoriは、農業流通現場のDXを実現するSaaS『nimaru』と青果店『KAJITSU』を運営する会社です。

React 18 へのアップグレード: 新バージョン非対応の依存ライブラリへの対応

こんにちは。 株式会社 kikitori でエンジニアをしている小川です。

kikitori では JA や卸売市場向けの SaaS「nimaru」を開発しています。

nimaru のフロントエンドは TypeScript + React で構築しており、ローンチから約5年間 React 17 を使用してきました。

このたびようやく React 17 を React 18 にアップグレードしたのでその手順と対応内容をまとめました。

主な対応内容

今回のアップグレード対応は、大きく以下の2点に分かれました。

  • アプリケーションコードの React 18 対応
  • 依存ライブラリの React 18 対応

アプリケーションコードの React 18 対応

React 17 から React 18 への変更内容は内部的な変更が主で、アプリケーションコードが直接影響を受ける変更はそれほど多くありません。

How to Upgrade to React 18

nimaru で対応が必要だったのは以下の3点です。

これらの変更は将来の仕様変更に備えての非推奨化だったので tsceslint による静的解析で検出でき、公式ガイドに沿って対応できました。

※ JSX 名前空間の変更 は React 19 で行われた変更でしたが、React 18.3 でも導入されたため同時に対応しました。

依存ライブラリの React 18 対応

React をアップグレードすると、当然ながら React に依存する各種ライブラリにも影響が出ます。 nimaru で利用しているライブラリのいくつかも React に依存しており、React 18 では不具合が発生したものがありました。

ほとんどの不具合は利用する API を変更するか同等の機能を持つ別のライブラリへの置き換えで解消できましたが、1つだけ代替手段がなく独自に対応する必要がありました。

react-qr-reader

nimaru ではアプリ内の各所でQRコードの読み取りを行っており、そのために react-qr-reader を使用していました。

しかし、React 18 では react-qr-reader で一度カメラを起動するとブラウザをリロードしない限りカメラを停止できないという不具合が発生しました。

react-qr-reader/cant use this library in react@18.2.0

このライブラリは3年以上更新されておらず、公式での修正も期待できない状況でした。幸いMITライセンスで公開されていたため、自分たちで修正を試みることにしました。

まずは GitHub の Issues に解決方法が投稿されていないかを確認しましたが、同様の不具合に関する報告は多数あったものの明確な対処法は見つかりませんでした。

react-qr-reader は、バーコード処理用のライブラリ zxing-js をラップして React コンポーネントとして提供する構成になっており、カメラの制御は主に zxing-js 側で行われています。

そのため zxing-js の Issue を確認したところ以下の投稿を見つけました。

How to stop the reader and turn off the camera

そして このコメント にメディアストリームの停止と video 要素のクリーンアップすることでカメラを停止する方法が投稿されていました。

この処理を react-qr-reader のカメラ停止処理に組み込みます。react-qr-reader では useEffect のクリーンアップ関数でカメラを停止しています。

// from: https://github.com/JodusNodus/react-qr-reader/blob/master/src/QrReader/hooks.ts

export const useQrReader: UseQrReaderHook = ({
  scanDelay: delayBetweenScanAttempts,
  constraints: video,
  onResult,
  videoId,
}) => {
  const controlsRef: MutableRefObject<IScannerControls> = useRef(null);

  ...

  useEffect(() => {
    ...

    return () => {
      controlsRef.current?.stop();
    }
  }, []);

この useEffect のクリーンアップ関数に先ほどの Issue で紹介されていたカメラ停止処理を追加します。 また依存配列も空配列のままだったため依存変数を追加しました。

  useEffect(() => {
    ...

    return () => {
      controlsRef.current?.stop();
+     controlsRef.current = null;
+     BrowserQRCodeReader.releaseAllStreams();
+     const videoElement = document.getElementById(videoId) as HTMLVideoElement;
+     if (videoElement) {
+       BrowserQRCodeReader.cleanVideoSource(videoElement);
+     }
    };
-  }, []);
+  }, [videoId, scanDelay, onResult, constraints]);

この修正により React 18 でも react-qr-reader が正常に動作するようになりました。

React 18 では useEffect の挙動が変更されており、Strict Mode 有効時は開発環境でセットアップとクリーンアップが2回実行されるようになりました。

コンポーネントのマウント時にエフェクトが 2 回実行される

この仕様変更によりクリーンアップ処理が不完全な場合は従来とは異なる挙動になる可能性があるとアナウンスされていました。 react-qr-reader のカメラの停止処理は useEffect のクリーンアップ関数で行われており、今回の不具合もこの仕様変更よるものと考えられます。

振り返って

nimaru で使用しているライブラリは定期的にアップデートしていますが、破壊的変更を含むメジャーバージョンアップはどうしても慎重にならざるを得ず後回しになりがちです。 今回ようやく重い腰を上げて React のアップグレードに取り組みましたがやはり一筋縄ではいきませんでした。

また React 18 への移行は一度にリリースするのではなく、React 17 でも適用可能な変更から少しずつ反映していく方針を取りました。 こうした段階的なリリースにより一度のリリースで発生しうる影響を最小限に抑え、他の機能開発を妨げることなくアップグレードを進められました。 もちろんリリースの分割には手間もかかりましたが、最終的には延べ8,000行以上のコードを変更することになったため結果としては正解だったと思います。