「ストアへの再申請なしで修正を配信できる」というのは EAS Update(OTA アップデート)の最大の利点ですが、いざ publish したのに「自分の iPhone で再起動してもアプリが古いまま」という現象には、私も何度か遭遇しています。
厄介なのは、EAS Update は 静かに失敗する という点です。eas update コマンド自体は緑色で「Published」と出るのに、ユーザーの端末にだけ届いていません。Sentry を見ても何も出ていません。こういう「成功っぽく見えるのに反映されない」パターンは、原因が複数のレイヤーに散らばっているため、思いつきで対処すると半日が溶けます。
ここではRork で生成したアプリに EAS Update を配信した経験から、私が実際に踏んだ「反映されない」パターンを5つに整理しました。最後にターミナルで5分以内に原因を切り分ける診断フローも置いてあります。
まず最初に確認すべきこと — 「ビルドプロファイル」と「アップデートチャネル」の対応関係
EAS Update の事故は、9割が 配信先と受信側のチャネルがズレている ことから来ます。eas.json のビルドプロファイル(development、preview、production)と、eas update 時に指定するチャネル名が一致していないと、ビルド済みアプリは更新を受け取れません。
# 現在公開中のチャネルを確認
eas channel:list
# 特定チャネルの最新アップデート履歴を確認
eas update:list --branch productioneas.json 側はこういう構造になっているはずです:
{
"build": {
"production": {
"channel": "production",
"autoIncrement": true
},
"preview": {
"channel": "preview",
"distribution": "internal"
}
}
}ここで production ビルドに対して eas update --branch preview を流していると、当然ですが production アプリには永遠に届きません。「自分は preview ビルドで確認している」のか「TestFlight 経由の production ビルドで確認している」のかを最初に確定させてください。
典型パターン①: runtime version が一致していない
EAS Update が反映されない原因として、私が一番よく踏むのがこれです。
EAS Update は「ネイティブコードを変えていないアプリにだけ JavaScript の差分を流す」仕組みなので、ネイティブコードのバージョン(runtime version)が完全一致しないと、たとえチャネルが合っていても更新を受け取りません。これは安全装置として正しい挙動なのですが、Expo SDK のアップグレードや new-architecture の有効化を挟むと runtime version が変わるため、古い端末は永久に置き去りになります。
app.json で runtime version の戦略を確認します:
{
"expo": {
"runtimeVersion": {
"policy": "appVersion"
}
}
}appVersion ポリシーの場合、app.json の version を変えるたびに runtime version も変わります。つまり 1.2.0 のアプリを公開済みのときに、ローカルで version を 1.3.0 に変えてから eas update を打つと、ストアの 1.2.0 ユーザーには 届きません。「ストアに出ている版に当てたいなら、ローカルの version をストアの版に戻してから publish する」のが鉄則です。
ストア側の version を確実に確認するには、App Store Connect / Google Play Console を見るのが一番早いです。
典型パターン②: クライアント側の expo-updates 設定が間違っている
app.json の updates.url と runtimeVersion、そして iOS / Android のネイティブ側に埋め込まれたチャネル ID — このどれかがビルド時にズレていると、アプリは「自分宛のアップデートはどこか」を見つけられません。Rork が生成したコードベースの場合、最初から正しく入っていることが多いのですが、後から手で編集してしまった場合は要注意です。
{
"expo": {
"updates": {
"url": "https://u.expo.dev/your-project-id",
"enabled": true,
"checkAutomatically": "ON_LOAD",
"fallbackToCacheTimeout": 0
},
"runtimeVersion": { "policy": "appVersion" }
}
}特に enabled: false になっていたり、checkAutomatically が ON_ERROR_RECOVERY になっていたりすると、起動のたびにはチェックしません。私はある案件で、デバッグの過程で enabled: false にしたまま production ビルドしてしまい、丸2日ハマりました。
典型パターン③: ユーザー側の端末がチェックタイミングを逃している
これは仕様で、なかなか気づきにくいポイントです。
expo-updates のデフォルト動作は「アプリ起動時に新しいアップデートをチェック → 見つけたら次回起動時に適用」という 2回起動が必要なフロー です。つまり、ユーザーが今アプリを開いていて、その状態で publish しても、すぐには切り替わりません。一度アプリを完全に閉じて(バックグラウンドではなく終了)、もう一度開く必要があります。
検証で「自分でも反映されない」と感じたら、まずアプリスイッチャーから完全に終了して2回開き直してください。それでも反映されないなら、別の原因です。
「即時反映」が要件のときは、起動後にアプリ内で能動的にチェック→再起動を行うコードを書きます:
import * as Updates from "expo-updates";
import { useEffect } from "react";
import { Alert } from "react-native";
export function useImmediateUpdate() {
useEffect(() => {
if (__DEV__) return; // 開発ビルドではスキップ
(async () => {
try {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
await Updates.fetchUpdateAsync();
Alert.alert(
"アップデートを取得しました",
"アプリを再起動して反映します。",
[{ text: "OK", onPress: () => Updates.reloadAsync() }]
);
}
} catch (e) {
// ネットワーク切断等は無視(次回起動時に再チェックされる)
console.warn("[updates]", e);
}
})();
}, []);
}期待する出力としては、publish 直後に対象端末でアプリを開き直すと、ダイアログが出てそのまま再起動 → 新しい JS が動く、という流れになります。__DEV__ チェックは必須です。これを忘れると、Rork や Expo Go で開発しているときに「アップデートはありません」のエラーが連発して、開発体験が壊れます。
典型パターン④: development ビルドにアップデートを流している
eas update --branch development で配信したアップデートは、eas build --profile development で焼いた development ビルドにしか届きません。これは当たり前なのですが、社内テスターに対して「TestFlight 配ってあるからアップデート届くよね」と思って development ブランチに publish してしまう事故が時々起きます。
確認手順は単純で、自分のテスト端末がどのプロファイルでビルドされたかを eas build:list で確認します:
eas build:list --platform ios --limit 5ここに表示される profile 列と、eas update --branch <name> の name を必ず一致させます。「TestFlight に流したのは preview プロファイルだったか production プロファイルだったか」を曖昧にしないことが、このパターンの予防策です。
典型パターン⑤: ビルドキャッシュ・端末側の埋め込み更新が悪さをしている
最後に、ここまで全部試しても反映されないときの最終手段です。
expo-updates は、起動時にダウンロード済みのアップデートを起動 → バックグラウンドで新しいアップデートを取得、というローテーションをします。何らかの理由で「古いダウンロード済みアップデートが残っている」場合、それが優先されることがあります。
切り分けるには、対象端末でアプリをいったんアンインストール → ストア(または TestFlight)から入れ直してください。これで「埋め込みバンドル」のみの状態に戻ります。その状態で起動して、反映されるかを見ます。
- 入れ直して反映される → クライアント側のキャッシュ問題
- 入れ直しても反映されない → publish 自体が届いていない(パターン①〜④のいずれか)
5分でできる診断フローチャート
慌てている時のために、最短の切り分け手順を置いておきます。
eas channel:listで publish 先のチャネルを確認eas update:list --branch <名前>で最新の publish が登録されているか確認- その publish の
runtimeVersionをメモする - 対象端末アプリの runtime version を確認(後述のコードでアプリ内に表示するのが楽)
- 一致していない → ローカルの
app.jsonの version を戻して publish し直す - 一致している → アプリを完全終了 → 2回起動して再チェック
- それでも来ない → アンインストール → ストアから入れ直し → 再チェック
アプリ側で runtime version を表示するには、設定画面などに以下を入れておくとデバッグが速いです:
import * as Updates from "expo-updates";
import { Text, View } from "react-native";
export function UpdateDebugInfo() {
return (
<View style={{ padding: 16 }}>
<Text>Runtime: {Updates.runtimeVersion}</Text>
<Text>Channel: {Updates.channel}</Text>
<Text>Update ID: {Updates.updateId ?? "(embedded)"}</Text>
<Text>Created: {Updates.createdAt?.toISOString() ?? "-"}</Text>
</View>
);
}この4行があるだけで、「今ユーザーが動かしているのは publish 何番のバンドルか」が即座に分かります。本番ビルドでは隠したい場合、ロングタップ5秒でモーダル表示するなどのデバッグメニューを仕込んでおくのが私の好みです。
関連する周辺の知識
EAS Update の挙動をもう少し体系的に理解したい場合は、まず Rork の EAS Update / OTA 配信ガイド で基本フローを押さえてから、CI/CD と組み合わせる場合は GitHub Actions と EAS Build を使った CI/CD パイプラインの構築 を読むと、publish までの自動化までつながります。ビルド自体が通らないというときは、別記事の React Native のビルドエラーを直すためのパターン集 を先に当たってください。
書籍で React Native と Expo の挙動を腰を据えて
全体を振り返ってにかえて — 次にやること
OTA トラブルの大半は、原因がコードではなく「設定の食い違い」にあります。なので、慌てて eas update を連打する前に、最初に手元で eas channel:list と eas update:list --branch <名前> を打って、「今 publish した内容が、ちゃんと自分が思っているチャネルに登録されているか」を確認するクセをつけてください。
今日すぐにできる一手として、開発中のアプリにさきほどの UpdateDebugInfo コンポーネントを組み込んでおくことをおすすめします。次に「反映されない」が起きたとき、ユーザーの端末で動いているバージョンを目視できるだけで、診断にかかる時間が桁で変わります。