🎄

フロントエンドの React 18 バージョンアップとtypescript導入の取り組み

2023/12/25に公開

この記事は OPENLOGI Advent Calendar 2023 18日目の記事です。

先日、オープンロジのフロントエンドの技術スタックを最新へと更新しました。この記事ではその経緯や詳細について共有します。

経緯

オープンロジのフロントエンドはおよそ10年間の開発を経て50万行を超える規模になりました。しかし技術スタックの改善がしばらくの間行えておらず、新規開発においても既存の修正においても開発者体験が良いとは言えない状況が続いていました。たとえば、

  • 複数のアプリケーションが一つのコードベースで管理されており、チームを跨いで共有している。またライブラリのバージョンアップへの影響が非常に大きい
  • すでに利用されていないファイルが数多く存在する(それにより不必要な修正が発生)
  • 使っていない大量のimport文
  • 無視され続けた10000件ほどのESLintエラー
  • Reactのバージョン15である
  • そのほか古いバージョンのライブラリが大量に利用されている

などなど、まぁ書き出してみると酷いものです。

フロントエンドをより楽しく、効率的に安全に開発できるように上記課題の改善に取り組み、

  • フロントエンドのコード分離
  • 不要なファイルやライブラリの削除
  • React 18へのバージョンアップ
  • TypeScriptの導入
  • ESLintの整備

を行いました。
細かい対応についてはまた別の記事を記載したいと思いますが、本記事ではこれらの対応の一部について、対応内容はポイントについて事例として紹介させて頂きます。

対応事例

コードベースの分離と不要なファイル・ライブラリの削除

弊社のフロントエンドアプリケーションは小さいものも含め4つのアプリケーションが存在しますが、全て一つのコードベースで管理をしていました。ライブラリのバージョンアップが複数のアプリケーション/チームに影響がでる状態で、これを改善するためまずはコードベースの分離を行いました。

一応エントリーポイントとなるファイルは分かれており、webpackを利用して最終的な成果物も分離されている状態です。そこで、以下の流れでアプリケーションごとに分離を行いました。

  1. 既存のソースコードを全て4つのディレクトリに複製し、完全に動作する状態で分離する
  2. eslint-plugin-unused-importsを用いて不要なimport分を全て削除
  3. dependency-cruiserを利用して、どこからもimportされていないファイルの抽出と削除
  4. npx depcheckにより、不要なライブラリの抽出と削除

React18へのバージョンアップ

基本的には公式のアップグレードガイドやBreaking Changeを確認して、逐次対応を行います。
その中でも特に影響の大きな点を紹介します。

prop-typesの移行

prop-typesの移行を行わないとモジュールの解決すら行えないため、優先して対応が必要です。ローカルのコードは公式ツールを使ってマイグレーションします。

https://legacy.reactjs.org/blog/2017/04/07/react-v15.5.0.html#migrating-from-reactproptypes

依存ライブラリの中にはreact18対応が行われず、古い実装のままのものがいくつかありますが、これらはforkした上で移行処理を行っています。

一部lifecycleメソッドの移行

同様に、componentWillMount などの一部のlifecycleメソッドが廃止になるため、 UNSAFE_*** への変更を行います。今後は UNSAFE_*** 自体も削除される予定ですが、今回のアップグレード時では一旦 UNSAFE_*** への移行を行い、別のタイミングで利用するlifecycleメソッドへの移行を行うことにしました。

https://legacy.reactjs.org/blog/2018/03/29/react-v-16-3.html#component-lifecycle-changes

componentWillMountの呼び出しタイミングの変更

この変更が触れられている記事は比較的少ないようですが、弊アプリケーションではいくつか対応が必要でした。

元々は削除されるコンポーネントの componentWillUnmount が呼ばれた後で、 新しいコンポーネントの コンストラクタrendercomponentWillMount が呼ばれていました。
react16以降では、新しいコンポーネントの componentWillMount が呼ばれた後で、削除されるコンポーネントの componentWillUnmount が呼ばれるようになります。

When replacing <A /> with <B />, B.componentWillMount now always happens before A.componentWillUnmount. Previously, A.componentWillUnmount could fire first in some cases.

https://legacy.reactjs.org/blog/2017/09/26/react-v16.0.html

ReactDOM.renderの廃止

本来react18の concurrent rendering に対応するには、Reactの描画処理である ReactDOM.render から React.createRoot への変更が必要となります。

この対応により、非同期処理においても状態更新処理( ≒ setState によるrendering)がバッチ処理となりレンダリング中の処理として呼ばれるようになります。

具体的には以下のような処理があった場合、

    <button onClick={() => {
      new Promise((resolve, reject) => {
	reject();
      }).catch(() => {
	console.log('catch 1');
	this.setState({
	  hoge: 3,
	}, () => {
	  console.log('catch callback 1');
	})
	console.log('catch 2');
	this.setState({
	  hoge: 4,
	}, () => {
	  console.log('catch callback 2');
	  throw new Error('unhandled in callback!');
	});
      });
    }
    }> hoge!!</button>

元々の動作は

> catch 1
> catch callback 1
> catch 2
> catch callback 2

の順で呼び出されるようになります。

しかし、concurrent renderingを有効にした状態では、

> catch 1
> catch 2
> catch callback 1
> catch callback 2

の順で呼び出される事になります。

これによって、 this.state に依存した書き方をしている場合に挙動への影響が発生します。

また、元々Promise内でのエラーは Unhandled Promise Rejection となりとしてReactのツリー描画に影響を及ぼすことはありませんでした。しかし、automatic batchingの処理により、上述のcallbackが後続のイベントループでの描画処理で実行される形になるため、Promise内でのエラーが直接ツリーの描画に影響します。

React16移行、レンダリング処理におけるエラーはコンポーネントツリーのアンマウントにつながるため、アプリケーションへの影響の大きな点となります。

TypeScript対応

webpackで babel-loader を利用し、コードのトランスパイルにはbabel-preset-typescriptを利用しました。
tsc による型チェックはCIとcommit hookによる対応を行っています。

また、ESLintは一新し、今後作成するTypeScriptのファイルについては厳格な運用ができるように拡張子毎に異なるルールを設けるようにしています。

  "overrides": [
    {
      "files": ["*.js"],
      "extends": ["./eslint/js.eslintrc.js"]
    },
    {
      "files": ["*.ts", "*.tsx"],
      "extends": ["./eslint/ts.eslintrc.js"]
    }
  ]

TypeScriptは strict での運用をしており、既存のJavaScriptファイルを呼び出す際は適宜 d.ts ファイルを作成しています。

まとめ

フロントエンドの改善についてまとめました。

改めて、Reactのバージョンアップ、TypeScriptの導入、ESLintの厳格な対応によってかなりの開発パフォーマンスや体験の向上を感じています。

進化が激しいフロントエンドの技術スタックは掘っておくとかなり味のある状況に陥ってしまします。これ以上ない状態からでも一つ一つ整理して進めれば新しい環境に生まれ変わらせることができます。
これからは計画性を持って、着実に改善を進めていければなと思っています。

OPENLOGI Tech Blog

Discussion