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プロパティのgap、rowGap、columnGapをサポートするようになりました(詳細はこちら)。 その他、アクセシビリティやスタイル、イベントに関してWebにインスパイアされたプロパティも追加されています(詳細はこちら)。
HermesはJSON.parse
のパフォーマンスが改善されました。
TypeScriptを使用している場合は、react-native
に型定義が含まれるようになったため、@types/react-native
を削除する必要があります。
詳細は、以下のリンク先を参照してください。
- React Native 0.71: TypeScript by Default, Flexbox Gap, and more...
- React Native CHANGELOG
- React CHANGELOG
JavaScriptのデフォルトエンジンをHermesに変更
HermesがJavaScriptのデフォルトエンジンになりました。
Hermesでは、アプリ起動後にユーザがアプリを使用できるまでの時間(TTI: Time to Interactive)が短縮されます。メモリ使用量も削減されるなど、パフォーマンの大きな改善が期待できます。
また、デバッグ機能もJavaScriptCore(JSC)に比べると大幅に改善されています。
詳細は、Hermes as the Defaultを参照してください。
全てのExpoモジュールでAPIレベルに33を設定
AndroidのcompileSdkVersion
とtargetSdkVersion
は、すべてのExpoモジュールでAPIレベル33に設定されました。
これは、2023/08/31以降に適用される(2023/11/01まで期限延長をリクエスト可能)Google Playアプリの対象APIレベル要件を満たしています。
Expo GoでRTL言語のサポート
Expo GoでRTL言語がサポートされました。app.json
やapp.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 Image v1.0
- React NativeのImageコンポーネントの後継として、またはreact-native-fast-imageの代替を意図して作成されたライブラリ
- モダンな画像フォーマットをサポート
- スピードが重視された設計
- placeholdersやtransitions、blurhashなど、既存のライブラリでは追加実装を必要とする場合があった機能などもサポート
- Expo Router v1.0
- ファイルシステムベースでルーティング可能なナビゲーションライブラリ
非推奨となるライブラリ
以下のライブラリが非推奨となりました。
- expo-random
- expo-randomは、expo-cryptoに統合されました
Expo Module APIの改善
async
/await
関数で、Swift Concurrencyがサポートされました。
また、TypedArray
のサポートも進められており、expo-crypto
のgetRandomValues
やdigest
で使用されています。
app.json
の非推奨となっていた項目の削除
entryPoint
がapp.json
から削除されました。代わりに、package.json
のmainを使用してください。
expo-auth-session
expo-auth-sessionのuseProxy
が非推奨になりました。
expo-keep-awake
expo-keep-awakeのactivateKeepAwake
が非推奨になりました。代わりに、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にアップグレードしました。
npm install expo@^48.0.0
を実行して、Expo SDKをアップグレードnpx expo install --fix
を実行して、Expoが管理するライブラリのアップグレード@expo/config-plugins
をdevDependencies
に追加jest-expo
のアップグレード- 既存のパッチファイルの更新
- Expoが管理するライブラリに依存するライブラリの更新
node_modules
、package-lock.json
を削除して、npm i
を実行- Expoの更新履歴の確認と対応
- React Nativeの更新履歴の確認と対応
- expo-template-blank-typescriptの更新履歴の確認と対応
- expo-template-bare-minimumの更新履歴の確認と対応
- React Native Upgrade Helperを参照して、React Nativeの更新を確認
- このアプリで必要な対応は、expo-template-bare-minimumの更新で対応されていました
- JavaScriptのエンジンをHermesに変更
npm run prebuild
を実行してネイティブプロジェクトの再生成FlatList
でデータが0件の場合にscrollToEnd
を実行するとエラーが発生する問題の対応- Jestのアップグレード対応
npm run android
でアプリが起動しない問題の対応- 手動管理しているライセンスの更新
アップグレードを実施したPull Requestはこちらです。
@expo/config-plugins
をdevDependencies
に追加
このアプリの依存ライブラリであるexpo
や@react-native-firebase/app
などは、@expo/config-plugins
をdependencies
に設定しています。
これらのライブラリが設定している@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-native | 18.1.0 | 18.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-awake
のactivateKeepAwake
が非推奨になったことへの対応
Expo SDK 48の主な変更 - Expoがサポートするライブラリや機能 - expo-keep-awakeに記載した通り、expo-keep-awake
のactivateKeepAwake
が非推奨になりました。そのため、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'
Omit
をtypescript
からではなく、react-native
からimportしていたためです。
これは、以下のようにreact-native
からのimportを削除して、typescript
で定義されている型定義を参照するように修正しました。
// 修正前
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.0
、npx 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は以下になります。
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
に変更しています。
// 修正前
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-native
の0.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__
配下のモックファイルは、モックしているライブラリなどがimport
、require
されたときに読み込まれます。
このアプリの一部のテストでは、テスト対象のコンポーネントの内部で、条件によって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
に設定しました。
import mockRNCNetInfo from '@react-native-community/netinfo/jest/netinfo-mock.js';
jest.mock('@react-native-community/netinfo', () => mockRNCNetInfo);
/* ~省略~ */
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を強制終了するようにしています。
{
/* ~省略~ */
"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に追加しています。