こんにちは。技術本部ウラカタ開発部のkaorun343です。フロントエンドエンジニアのチームでは継続的に開発環境の改善活動をおこなっています。今回はこの活動において実施した、React Routerのアップデート作業について紹介します。
はじめに
アソビューの社内向け管理画面はReact Routerを用いたシングルページアプリケーションとして構築しています。この管理画面アプリケーションは長らく依存パッケージの更新がなされていなかったため、React 16、React Router 5.1に依存しています。このアプリケーションは今後も継続的に機能追加がなされるため、依存パッケージを更新し、開発者生産性の向上を目指すことにしました。
React Routerの最新のメジャーバージョンであるReact Router 6では、Data API、すなわちloader
関数や action
関数を利用できます。これにより、コンポーネントの実装とAPI通信の呼び出しを分離し、コンポーネント実装を簡潔にできます。そこで、今回はまだReact Router 5を使っているアプリケーションのReact Router 6へのアップデートに取り組みました。
方針
移行ガイドにそって更新作業を実施する
React Routerの公式ドキュメントはReact Router 5から6への移行ガイドを公開しています。こちらを参考に、アプリケーションのソースコードを修正しました。
アプリケーションの規模からReact Router 5から6へいきなり切り替えることが難しいと判断したので、react-router-v5-compatの導入を目標にしました。このパッケージを導入することで、React Router 5の中で、React Router 6のAPIを使えるようになります。したがって段階的に置き換えを進めることができます。
ESLintを用いる
React Routerの移行のために書き換えが必要な箇所は、ESLintを使って検出することにしました。
ESLintを用いた理由は、no-restricted-syntax
ルールを使えば追加のパッケージインストールが不要であったためです。
ESLintにおいては文法はESTreeで表現しています。そして、ESLintはCSSのセレクタのようなESTree向けのセレクタ記法を利用できます。今回は、no-restricted-syntax
を用いて、ESTree向けのセレクタを記述して移行対象を検出します。
コードのAuto Fix機能については、カスタムルールを定義する必要があるため実施しませんでした。検出のみを自動化していくことにしました。
AST Grepについて
また、ASTを元にしたツールとしてはAST Grepがありますが、こちらはTypeScriptおよびTSXに対して使用可能です。一方で、JavaScriptおよびJSXでは使えません。今回のアプリケーションはJavaScriptファイルを多く抱えているため、AST Grepは採用できませんでした。
ESLintを活用した移行作業
ルート定義の修正
React Router 6においては、<Routes>
の children
要素には <Route>
のみ含めることが可能です。従ってReact Router 5からの移行を容易にするために、 <Switch>
の子要素を <Route>
のみにすることから始めました。
<Switch>
の中にある <Redirect>
を除去
初めに取り組んだのは、リダイレクトの実装の変更です。
React Router 5では <Switch>
の children
要素に <Redirect>
を配置することが可能でした。これにより、特定のパスのリクエストが来た時にリダイレクト処理を実現していました。
React Router 6においては、<Route>
に渡すコンポーネントのなかで <Navigate>
を使うことになります。したがって、<Redirect>
から <Navigate>
への変更だけで済むように実装を変更しました。
<Switch>
直下の <Redirect>
を検出できるセレクタは以下のように書きました。
{ "no-restricted-syntax": [ "error", { "selector": "JSXElement[openingElement.name.name='Switch'].children > JSXElement[openingElement.name.name='Redirect']" } ] }
- まず、
<Switch>
コンポーネント、すなわちJSXで、名前がSwitch
の要素を探します。これはJSXElement[openingElement.name.name='Switch']
と書きます。openingElement
はJSXOpeningElement
で、 JSXの開始タグを表します。openingElement.name
はJSXIdentifier
で.name
に名前を持っており、加えて開始位置や終了位置の情報を持っています。 - 次に、この直接の子要素を選択したいので、
.children
を末尾に追加します。 <Switch>
要素の直接の子要素にある<Redirect>
を探したいので、JSXElement[openingElement.name.name='Switch']
を追加します。
これにより、該当箇所を検出できました。
実装の変更前と変更後です。
// Before <Switch> <Redirect exact from='/' to='/home' /> </Switch> // After <Switch> <Route path="/" render={() => <Redirect to="/home" />} /> </Switch>
<Switch>
の中にある <Route>
以外のコンポーネントの除去
前項では <Redirect>
を使っている箇所を書き換えましたが、それ以外の子要素も移行を容易にするために置き換えていきました。
{ "no-restricted-syntax": [ "error", { "selector": "JSXElement[openingElement.name.name='Switch'].children > JSXElement[openingElement.name.name!='Route']" } ] }
前項では <Redirect>
だけ検出していたので openingElement.name.name='Redirect'
としていましたが、ここでは <Route>
以外を検出したいので openingElement.name.name!='Route'
に変更しました。
検出後は、レイアウトが崩れないようにコンポーネントを修正していきました。
コンポーネントの修正
ルート定義の修正が終わったので、次にコンポーネントの修正に取り組みました。
route propsを利用している箇所の修正
React Router 5では <Route>
に渡したコンポーネントのpropsにroute props (match
、 history
そして location
)が渡されます。この仕様は React Router 6で廃止となりました。
route propsを利用している箇所を検出するために、最初は <Route component={MyPage} />
のように component propを使っている箇所を検出し、 <Route children={<MyPage />}>
に書き換える方法を考えました。このように書き換えることで、 <MyPage>
で route propsを利用していれば「propsを渡していない」とTypeScriptが型チェックでエラーを出してくれます。
しかしながら、react-reduxの connect
関数を利用しているコンポーネントがあり、Reduxのストアのオブジェクトを渡していないとTypeScriptで型エラーとなります。React Routerの移行に必要な最小限の修正に留めるため、今回は採用しませんでした。
代わりに、route propsを利用している箇所を検出する方針を採用しました。 <Route>
に渡すコンポーネントは名前の末尾に Page
をつけており( 例: ReservationPage
)、かつアロー関数を使っていたので、検出の条件に加えました。
{ "selector": "VariableDeclarator[id.name=/Page$/] > ArrowFunctionExpression > ObjectPattern Identifier[name=/^(match|params|location)$/]" }
検出した後は、以下の通り新しいAPIに移行しました。
match
からuseMatch
params
からuseParams
location
からuseLocation
最後に
今回はESLintを活用したReact Router移行の手法を紹介しました。このようにESLintなど静的解析ツールを用いた移行作業は他のパッケージにおいても活用できると思います。ぜひ試してみてください。
アソビュー株式会社では新しいメンバーを随時募集していますので、ご興味ある方はぜひご連絡ください。