メインコンテンツまでスキップ

Expo SDK 48アップグレード

参考

以下の記事を参考にして、このアプリのExpo SDKを48にアップグレードしました。 主な変更点とこのアプリで実施したアップグレード手順を紹介します。

なお、このアプリでは使用していないため、EASやClassic Updatesに関する内容は記載しません。

Expo SDK 48の主な変更

React Native 0.71.6 and React 18.2.0

React NativeのレイアウトエンジンがFlexboxプロパティのgaprowGapcolumnGapをサポートするようになりました(詳細はこちら)。 その他、アクセシビリティやスタイル、イベントに関してWebにインスパイアされたプロパティも追加されています(詳細はこちら)。

HermesはJSON.parseのパフォーマンスが改善されました。

TypeScriptを使用している場合は、react-nativeに型定義が含まれるようになったため、@types/react-nativeを削除する必要があります。

詳細は、以下のリンク先を参照してください。

JavaScriptのデフォルトエンジンをHermesに変更

HermesがJavaScriptのデフォルトエンジンになりました。

Hermesでは、アプリ起動後にユーザがアプリを使用できるまでの時間(TTI: Time to Interactive)が短縮されます。メモリ使用量も削減されるなど、パフォーマンの大きな改善が期待できます。

また、デバッグ機能もJavaScriptCore(JSC)に比べると大幅に改善されています。

詳細は、Hermes as the Defaultを参照してください。

全てのExpoモジュールでAPIレベルに33を設定

AndroidのcompileSdkVersiontargetSdkVersionは、すべてのExpoモジュールでAPIレベル33に設定されました。

これは、2023/08/31以降に適用される(2023/11/01まで期限延長をリクエスト可能)Google Playアプリの対象APIレベル要件を満たしています。

Expo GoでRTL言語のサポート

Expo GoでRTL言語がサポートされました。app.jsonapp.config.jsで有効化する方法は、Enabling RTL Supportを参照してください。

New Architecture/Fabricのサポート

expo-dev-clientを除く全てのExpoモジュールで、New Architecture/Fabricがサポートされました。

ただし、New Architectureをサポートしていないサードパーティのモジュールが多いため、New Architectureの有効化はまだ推奨されていないようです。 また、Expo GoはNew Architectureをサポートしていません。

Expoがサポートするライブラリや機能

ライブラリの更新

Expoが管理しているライブラリの内、メジャーバージョンアップなど大きなリリースがあったものを記載します。

非推奨となるライブラリ

以下のライブラリが非推奨となりました。

Expo Module APIの改善

async/await関数で、Swift Concurrencyがサポートされました。

また、TypedArrayのサポートも進められており、expo-cryptogetRandomValuesdigestで使用されています。

app.jsonの非推奨となっていた項目の削除

entryPointapp.jsonから削除されました。代わりに、package.jsonmainを使用してください。

expo-auth-session

expo-auth-sessionuseProxyが非推奨になりました。

expo-keep-awake

expo-keep-awakeactivateKeepAwakeが非推奨になりました。代わりに、activateKeepAwakeAsyncを使用してください。

@react-native-community/datetimepicker

@react-native-community/datetimepickerの以下の項目が非推奨になりました。

  • positiveButtonLabel
  • negativeButtonLabel
  • neutralButtonLabel

代わりに、以下の項目が追加されています。

  • positiveButton
  • negativeButton
  • neutralButton

Expo SDK 45のサポート終了

Expo SDK 45がサポート対象外になりました。Expo SDK 45を使用している場合は、Expo SDK 46以降にアップグレードする必要があります。

なお、次のリリースではExpo SDK46と47のサポートが終了する見込みのようです。

このアプリで実施したアップグレードの手順

このアプリでは、以下の作業を上から順に実施してExpo SDK 48にアップグレードしました。

アップグレードを実施したPull Requestはこちらです。

@expo/config-pluginsdevDependenciesに追加

このアプリの依存ライブラリであるexpo@react-native-firebase/appなどは、@expo/config-pluginsdependenciesに設定しています。 これらのライブラリが設定している@expo/config-pluginsのバージョンは様々です。

  • expo: 6.0.1
  • @react-native-firebase/app: ^5.0.4

複数のバージョンが混在している場合、dedupedが発生しnode_modules配下は以下のようになります(必要な箇所以外は省略しています)。

node_modules/
├── @expo
│   ├── cli
│   │   ├── node_modules
│   │   │   ├── @expo
│   │   │   │   ├── config-plugins(6.0.1)
│   ├── config-plugins(5.0.4)
├── @react-native-firebase
│   ├── app

このアプリでは、@expo/cliが使用する@expo/config-pluginsに以下のパッチを適用しますが、dedupedが発生するかどうかによってパッチを適用するべきパスが変わってしまいます。

そのため、@expo/cliが使用する@expo/config-pluginsと同じバージョンをdevDependenciesに追加して、node_modules配下の構成を以下のようにしました。

node_modules/
├── @expo
│   ├── cli
│   ├── config-plugins(6.0.1)
├── @react-native-firebase
│   ├── app
│   │   ├── node_modules
│   │   │   │   ├── config-plugins(5.0.4)

この対応により、パッチを適用するパスは常にnode_modules/@expo/config-pluginsになります。

jest-expoのアップグレード

以前は、expo-cli upgradeを使用してExpo SDKのアップグレードを実施していました。しかし、Expo SDK 48 - Upgrading your appを確認すると、アップグレード手順が変更されていました。

# Expo SDK 47へのアップグレード手順
expo-cli upgrade

# Expo SDK 48へのアップグレード手順
npm install expo@^48.0.0
npx expo install --fix

上記コマンドを実行すると、expo-cli upgradeでは自動更新されていたjest-expoが更新されませんでした。そのため、jest-expoに関しては手動で^48.0.0に更新しました。

なお、上記事象に関してはExpoのForumsに質問を投稿しています。

既存のパッチファイルの更新

このアプリでは、patch-packageを使用して、以下のライブラリにパッチファイルを適用していました。パッチ内容の詳細は、こちらを参照してください。

  • @expo/config-plugins
  • expo-splash-screen
  • react-native-elements

@expo/config-pluginsの以下のコミットによる変更で、パッチファイルが適用できなくなっていたため、パッチファイルを再作成しました。 変更は改行の増減のみで、機能的な更新はありませんでした。

expo-splash-screenは、Expo SDKのアップグレードに伴いバージョンが上がりました。しかし、適用していたパッチファイルはまだ必要な対応だったため、パッチファイルは削除せずに各ライブラリのバージョンに合わせてファイル名をリネームしました。

上記以外のライブラリに関しては、バージョンが変わらなかったため変更はありません。

Expoが管理するライブラリに依存するライブラリの更新

Expoが管理するライブラリの更新に伴い、以下のライブラリのバージョンを更新しました。

ライブラリ名更新前更新後
@testing-library/react-native18.1.018.2.0
@types/jest<27.0.0<30.0.0

Expoの更新履歴の確認と対応

ExpoのCHANGELOGを参照して、Expo SDKとExpoが管理するライブラリの更新内容を確認しました。

expo-randomからexpo-cryptoへの移行

Expo SDK 48の主な変更 - Expoがサポートするライブラリや機能 - 非推奨となるライブラリに記載した通り、expo-randomは、expo-cryptoに統合されました。 そのため、依存ライブラリからexpo-randomを削除しています。

また、expo-randomを使用していた箇所は、expo-cryptoを使用するように変更しています。

// 修正前
import * as Random from 'expo-random';
// 修正後
import * as Crypto from 'expo-crypto';

expo-keep-awakeactivateKeepAwakeが非推奨になったことへの対応

Expo SDK 48の主な変更 - Expoがサポートするライブラリや機能 - expo-keep-awakeに記載した通り、expo-keep-awakeactivateKeepAwakeが非推奨になりました。そのため、activateKeepAwakeAsyncを使用するように変更しています。

// 修正前
await activateKeepAwake();
// 修正後
await activateKeepAwakeAsync();

@react-native-community/datetimepickerの一部の項目が非推奨になったことへの対応

Expo SDK 48の主な変更 - Expoがサポートするライブラリや機能 - @react-native-community/datetimepickerに記載した通り、@react-native-community/datetimepickerの一部の項目が非推奨になっています。

このアプリではneutralButtonLabelを使用していたため、以下のように変更しています。

// 修正前
pickerItemsProps={{neutralButtonLabel: m('消去')}}
// 修正後
pickerItemsProps={{neutralButton: {label: m('消去')}}}

React Nativeの更新履歴の確認と対応

React NativeのCHANGELOGを参照して、React Nativeの更新内容を確認しました。

@types/react-nativeを依存ライブラリから削除

Expo SDK 48の主な変更 - React Native 0.71.6 and React 18.2.0に記載した通り、react-nativeに型定義が含まれるようになりました。 そのため、依存ライブラリから@types/react-nativeを削除しています。

上記対応をしたところ、OverlayBackdrop.tsxで以下の型エラーが発生しました。

TS2305: Module '"react-native"' has no exported member 'Omit'

Omittypescriptからではなく、react-nativeからimportしていたためです。 これは、以下のようにreact-nativeからのimportを削除して、typescriptで定義されている型定義を参照するように修正しました。

OverlayBackdrop.tsx
// 修正前
import {Omit, Pressable, PressableProps, StyleSheet, View, ViewProps} from 'react-native';
// 修正後
import {Pressable, PressableProps, StyleSheet, View, ViewProps} from 'react-native';

expo-template-blank-typescriptの更新履歴の確認と対応

expo-template-blank-typescriptの更新履歴を確認しました。

npm install expo@^48.0.0npx expo install --fixで更新される依存ライブラリのアップグレードが主な変更でした。そのため、このアプリで特別な対応は必要ありませんでした。

expo-template-bare-minimumの更新履歴の確認と対応

expo-template-bare-minimumの更新履歴を確認しました。

このアプリではConfig Pluginsに対応しているので、expo-template-bare-minimumの更新に伴う個別の対応は基本的に必要ありません。 ただし、以下の場合は個別に対応する必要があります。

  • このアプリで作成しているConfig Pluginsによる変更と、expo-template-bare-minimumの更新に伴う変更が競合した場合
  • Prebuild時に生成されないファイル

上記内容を観点にexpo-template-bare-minimumの更新履歴を確認しました。

.buckconfigの削除と.gitignoreの更新

expo-template-bare-minimumの以下のコミットで、.buckconfigが削除され、.gitignoreが更新されていました。

このアプリで修正した.gitignoreのdiffは以下になります。

.gitignore
 local.properties
*.iml
*.hprof
+*.keystore
+!debug.keystore

# node.js
#
@@ -37,12 +39,6 @@ node_modules/
npm-debug.log
yarn-error.log

-# BUCK
-buck-out/
-\.buckd/
-*.keystore
-!debug.keystore
-
# Bundle artifacts
*.jsbundle

@@ -50,6 +46,9 @@ buck-out/
/.bundle/vendor
/ios/Pods/

+# Temporary files created by Metro to check the health of the file watcher
+.metro-health-check*
+
# Expo
.expo/
web-build/

JavaScriptのエンジンをHermesに変更

Expo SDK 48の主な変更 - JavaScriptのデフォルトエンジンをHermesに変更に記載した通り、Expo SDK 48からはHermesがデフォルトのJavaScriptエンジンになりました。

そのため、このアプリもJavaScriptのエンジンをHermesに変更しています。

app.config.js
// 修正前
jsEngine: 'jsc'
// 修正後
jsEngine: 'hermes'

なお、デフォルトがHermesなのであえてjsEngineを指定する必要はありません。このアプリでは使用しているJavaScriptエンジンを明示するために、jsEngineを設定しています。

FlatListでデータが0件の場合にscrollToEndを実行するとエラーが発生する問題の対応

react-nativeのアップグレードに伴い、FlatListでデータが0件の場合にscrollToEndを実行すると以下のエラーが発生しました。

ERROR Invariant Violation: Tried to get frame for out of range index -1, js engine: hermes

この問題に対するissueとPull Requestは以下になります。

Pull Requestは既にクローズされmainブランチにはマージされていますが、react-native0.71.6には含まれていません。 そのため、patch-packageを使用して、Pull Requestで対応されている内容を含んだパッチファイルを作成しました。

Jestのアップグレード対応

Jestは26系から29系にアップグレードされ、メジャーバージョンが3つも上がりました。 デフォルトのテストランナーがjest-jasmine2からjest-circusに変更されるなど、更新内容は多岐にわたります。

更新内容の詳細は以下を参照してください。

このアプリでは、いくつかの自動テストが失敗するなど修正を必要とする箇所がありました。

jest/__mocks__配下のモックファイルで実施しているbeforeEachでエラー

一部のテストを実行したところ以下のエラーが発生しました。jest/__mocks__配下のモックファイルで実行しているbeforeEachでエラーが発生しています。

Error: Hooks cannot be defined inside tests. Hook of type "beforeEach" is nested within

jest/__mocks__配下のモックファイルは、モックしているライブラリなどがimportrequireされたときに読み込まれます。 このアプリの一部のテストでは、テスト対象のコンポーネントの内部で、条件によってrequireするライブラリを切り替えています。 それが原因でこのエラーが発生したと考えられます。

この問題は、Expo SDK 48のアップグレードの過程で検出した問題ですが、以前のバージョンでも期待通りの動作にはなっていませんでした(エラーログとして出力されていないだけでした)。

そのため、Expo SDK 48のアップグレードに関するPull Requestではなく、以下のPull Requestで別途対応しました。

@react-native-community/netinfoを使用しているコンポーネントのテストでエラー

@react-native-community/netinfoを使用しているコンポーネントのテストで以下のエラーが発生しました。

Cannot read properties of undefined (reading 'isInternetReachable')
TypeError: Cannot read properties of undefined (reading 'isInternetReachable')
at InternetReachability.isInternetReachable [as update] (~/dev/src/github.com/ws-4020/mobile-app-crib-notes-expo-48/example-app/SantokuApp/node_modules/@react-native-community/netinfo/lib/commonjs/internal/internetReachability.ts:148:20)
at State.update (~/dev/src/github.com/ws-4020/mobile-app-crib-notes-expo-48/example-app/SantokuApp/node_modules/@react-native-community/netinfo/lib/commonjs/internal/state.ts:74:32)
at Generator.next (<anonymous>)
at asyncGeneratorStep (~/dev/src/github.com/ws-4020/mobile-app-crib-notes-expo-48/example-app/SantokuApp/node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:24)
at _next (~/dev/src/github.com/ws-4020/mobile-app-crib-notes-expo-48/example-app/SantokuApp/node_modules/@babel/runtime/helpers/asyncToGenerator.js:22:9)

対応としては@react-native-community/netinfoをモック化するJestのセットアップファイルを作成し、それをjest.config.jsに設定しました。

jest/setup/netinfo.js
import mockRNCNetInfo from '@react-native-community/netinfo/jest/netinfo-mock.js';

jest.mock('@react-native-community/netinfo', () => mockRNCNetInfo);
jest.config.js
/* ~省略~ */
setupFiles: [
'<rootDir>/jest/setup/global.js',
'<rootDir>/jest/setup/netinfo.js', // <- 追加
/* ~省略~ */

doneコールバックを使用しているテストは非同期関数にできない

Jest27以降では、doneコールバックを使用しているテストでasync/awaitを使用した非同期関数を使用できません。

// <- doneコールバックを使用する場合は、asyncを付けられない
it('200ms経ってからスプラッシュスクリーンが閉じられること', async done => {})

一部のテストでは、async/awaitとdoneコールバックを併用していたため、そのテストではdoneコールバックを使用しないように修正しました。

Fake Timersのデフォルトがmodernに変更

Jest27以降では、Fake Timersのデフォルトがmodernに変更されました。併せて、jest.useFakeTimersの引数にmodernを設定できなくなりました。

そのため、modernなFake Timersを使用していた箇所を以下のように修正しています。

// 修正前
jest.useFakeTimers('modern');
// 修正後
jest.useFakeTimers();

なお、legacyなFake Timersを使用したい場合はjest.useFakeTimers({legacyFakeTimers: true})のようにします。

Jest実行時のオプションの変更

@mswjs/dataを使用しているコンポーネントのテストで以下のエラーが発生しました。

Jest has detected the following 3 open handles potentially keeping Jest from exiting:

● MESSAGEPORT

9 |
10 | export const initialDb = () => {
> 11 | db = factory(models);
| ^
12 | maxDb = factory(models);
13 | minDb = factory(models);
14 | };

at Object.sync (node_modules/@mswjs/data/lib/extensions/sync.js:74:19)
at factory (node_modules/@mswjs/data/lib/factory.js:50:12)
at initialDb (src/fixtures/msw/db.ts:11:15)
at Object.<anonymous> (src/apps/app/App.test.tsx:17:12)
at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:24)
at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:22:9)
at node_modules/@babel/runtime/helpers/asyncToGenerator.js:27:7
at Object.<anonymous> (node_modules/@babel/runtime/helpers/asyncToGenerator.js:19:12)

調査して見ると、@mswjs/dataの内部で使用しているBroadcastChannelがクローズされていないことが原因でした。 該当箇所はこちらです。

これは、Jest実行時のオプションとして--detectOpenHandlesを指定することで検出できる問題です。--detectOpenHandlesは、クローズされていないリソースを検出してコンソールに出力するオプションです。

Jestのドキュメントには--detectOpenHandlesの説明として以下も記載されています。

このオプションには重大なパフォーマンスの低下があり、デバッグにのみ使用する必要があります。

そのため、CI環境やnpm run test実行時には--detectOpenHandlesを指定しない方が良いという意見が挙がりました。また、モバイルアプリにおいては、以下の理由によりリソースのクローズ漏れがサーバサイド程クリティカルな問題にはならないという意見もありました。

  • モバイルアプリは使用者の端末で動作するため、リソースのクローズ漏れによる影響がサーバサイドと比較して小さい
  • モバイルアプリは停止、または再起動が比較的頻繁に行われるので、リソースのクローズ漏れが長時間継続することは少ない

上記理由により、このアプリでは--detectOpenHandlesを指定しないことにしました。

また、Jestはリソースがクローズされていない場合にプロセスが終了しません。そのため、--forceExitを指定して、Jestを強制終了するようにしています。

package.json
{
/* ~省略~ */
"scripts": {
/* ~省略~ */
"test": "jest --runInBand --forceExit",
"test:coverage": "jest --runInBand --forceExit --coverage",
"test:report": "jest --runInBand --forceExit --reporters=default --reporters=jest-junit --coverage",
/* ~省略~ */
}
}

なお、今回の検討をする原因となった@mswjs/dataのクローズ漏れについては、@mswjs/dataをプロダクションで使用する可能性がほとんどないことから、特に対応はしていません。

npm run androidでアプリが起動しない問題の対応

Expo SDK 48にアップグレード後、npm run androidを実行すると以下のエラーが発生してアプリが起動しませんでした。 AndroidのAPIレベルが33の場合に発生する問題のようです(APIレベルが32のエミュレータで確認したところ、問題なく起動できました)。

› Opening jp.fintan.mobile.SantokuApp.local/.MainActivity on Pixel_3a_API_33
CommandError: Couldn't open Android app with activity "jp.fintan.mobile.SantokuApp.local/.MainActivity" on device "Pixel_3a_API_33".
The app might not be installed, try installing it with: npx expo run:android -d Pixel_3a_API_33
Activity class {jp.fintan.mobile.SantokuApp.local/jp.fintan.mobile.SantokuApp.local.MainActivity} does not exist.

アプリのビルドは成功し、エミュレータにはインストールされているので、このアプリでは以下の運用で回避することにしました。

  • エミュレータにインストールされたアプリを手動で起動
  • npm startを実行してMetroを起動

なお、この問題は以下のissueでExpoに報告しています。

手動管理しているライセンスの更新

このアプリでは、使用しているライブラリのライセンス一覧を出力するスクリプトを用意しています。詳細は、こちらを参照してください。

managed-license.jsの更新

ライセンス情報が不足しており補完したい、あるいは、開発時のみ使用するため除外したいライブラリとバージョンをmanaged-license.jsで管理しています。

ライセンス情報が不足しているライブラリなどは、以下のコマンドを実行することで確認できます。

  • node .script/check-licenses.js

実行した結果、いくつかのライブラリのライセンス情報を更新する必要がありました。 また、JavaScriptのエンジンをHermesに変えたことで、明示的に除外していたAndroid JSCのライセンス設定も不要になったので、managed-license.jsから削除しました。

ライセンスの表記ゆれに対応

このアプリでは、アプリに表示するライセンス名をSPDXに合わせています。

今回のExpo SDKアップグレードでは、一部のライセンスでSPDXに定義されていないライセンス名を使用しているライブラリがありました。 そのライセンス名(BSD 3-Clause)をlist-dependencies.jsに追加しています。