あなたは、まずアプリが動作するようになったところで、ユーザーがサインインした後、製品が再エンゲージフローのようなネイティブな感覚を求めていると思います。 カートのリマインダー。 レビューの促進。 新しいメッセージのアラート。 リリースのアナウンス。 最初のインスピレーションは、たとえ「プッシュを簡単に組み込む」だけでも、1週間後には、1台のデバイスがアラートを受け取るのに対して、シミュレーターが正常に登録されているのに、誰も説明できない理由でタップが正しい画面を開くことができないことになることがよくあります。
Expoプッシュ通知は、単純で魅力的なものか、驚くほど脆弱なものかです。
Expoは、React NativeチームにAPNsとFCMの上位層を提供するため、多くのチームが使用するのはその理由です。 ただし、デモと生産性のある実装の間のギャップは実際に存在します。 トークンライフサイクル、パーミッションのタイミング、リスナー設定、ペイロードの設計、バックエンドのクリーンアップなど、すべての要素が重要です。 さらに頻繁にアプリロジックを変更する場合、信頼性の高いメッセージングとリリースの速度が依存する場合、運用上の規律がさらに重要になります。 これは、信頼性の高いメッセージングとリリースの速度が依存する場合、運用上の規律がさらに重要になります。 モバイルアプリユーザーロイヤルティーワーク:ユーザー体験の予測可能性が高くなるのは、配信が有効であることの条件です。
目次
- ユーザーとのつながりを築くためのエクスポ推送通知の基盤
- プロジェクトの初期設定と構成
- パーミッションの要求とプッシュトークンのキャプチャ
- サーバーから通知を送信する
- アプリ内で受信通知を処理する
- 生産性の向上と一般的な誤り
Expo Push Notificationとユーザーの関与の基礎
An Expo Push Notification セットアップは、1つの理由で魅力的です。複雑なネイティブメッセージングをチームが最初の日から所有したくないため、多くのネイティブメッセージングの複雑さを削減します。APNsとFCMの直接プラumbingを最初に構築するのではなく、Expoのゲートウェイと協力して、製品の動作、ルーティング、許可のUX、バックエンドのメッセージロジックに焦点を当ててください。
抽象化は、Pushが「実際に」なり得ないことを意味するのではなく、エンジニアリングの努力がどこに移動するかを変えるだけです。
サービスは、パフォーマンスが心配されることよりも速いです。2023年3月14日から2023年6月12日までの期間、ExpoのPush Notification APIは、 42ミリ秒のmedian応答時間, 273ミリ秒のp99遅延、そして 平均日間エラー率0.17% across CapawesomeとCapgoの違い数百万の日々のメッセージ Knock’s Expo push API benchmark analysisKnockのExpoプッシュ__CAPGO_KEEP_0__ベンチマーク分析
。これは、Expoがプロトタイプにのみ適していることを疑問視するチームに安心感を与えるはずです。
実際にはExpoが抽象化するものは
- チームが「Expoプッシュ」と言っている場合、多くの場合、複数の別々の懸念をまとめて言っていることです: プロバイダーのルーティング: Expoはメッセージを iOSと FCM Android用。
- トークン形式: サーバーはExpo Push Tokenを管理する代わりに、プラットフォーム固有のトークンハンドリングを初期化するのではなく、トークンを保存して送信します。
- リクエスト契約: ExpoのプッシュAPIにペイロードをPOSTしますが、直接ネイティブプロバイダーのAPIに統合するのではなく。
実際は、多くの失敗はアプリcode、古いトークン、不正なペイロード、または許可フローの不十分さから生じます。
実践的なルール: Expoを信頼できるトランスポート層として扱い、クライアントとバックエンドの設計に問題がないことを確認すること。
実際の生産性とは何か
動作するデモは、1台のデバイスが1回のペイロードを受け入れたことを証明するだけです。生産性が実際に意味するものは別のものです:
| 問題 | デモの考え方 | 実稼働の考え方 |
|---|---|---|
| 許可 | 即時で質問する | ユーザーの価値が明確になるまで、コンテキスト内で質問する |
| トークン | 一度だけ保存 | 更新、重複排除、期限切れ、整合 |
| ペイロード | すべてを含める data |
ペイロードを小さく、行動指向にする |
| アプリの動作 | アラートを表示 | 正しくナビゲートし、前景状態を処理 |
| オペレーション | 手動テスト | 受領、クリーンアップ、ログ、インシデントの処理 |
「通知を送信する」と「通知をサポートする」というのは、どちらが「製品ワークフローをサポートする」かという違いです。
プロジェクトの初期設定と構成
Expoの通知の多くの痛みは、最初の許可のプロンプトの前に始まります。プロジェクトの構成が雑だとすると、クライアントcodeが正しく見えるときに、アプリはビルドごとに一貫して動作しない可能性があります。

正しいライブラリがインストールされていることと、ビルドパスに合った開発環境から始めましょう。Expo Goを超えて作業している場合、ローカルワークフローをカスタムExpo開発クライアントのセットアップと同期させることが役立ちます プロジェクトの初期設定と構成、通知の動作がビルドで検証される必要があるのはよくあることなので、実際の生産環境に近いものに近づけるには、サンドボックスの実行ではありません。
通知パッケージをインストールする
最低限、許可の要求、トークン取得、リスナー、通知の表示が必要です。
expo-notificationsトークン取得を守る必要があるのはexpo-deviceインストールコマンドはパッケージマネージャーに依存しますが、主なことはバージョンが __CAPGO_KEEP_0__ と同じであることです。混在させないでください。エクスポが互換性のあるバージョンを解決するようにしてください。Device.isDevice.
Typical install commands depend on your package manager, but the key is version alignment with your Expo SDK. Don’t mix arbitrary package versions. Let Expo resolve compatible ones.
設定を明確に保ちましょう。最小限の
または app.json or app.config.js エクスポのバージョンと同じであることを確認してください。
{
"expo": {
"name": "MyApp",
"slug": "my-app",
"plugins": ["expo-notifications"],
"ios": {
"bundleIdentifier": "com.example.myapp"
},
"android": {
"package": "com.example.myapp"
},
"extra": {
"eas": {
"projectId": "your-project-id"
}
}
}
}
プロジェクトのレベルで設定を追加する
- 設定を明確に保ちましょう。最小限の 実際に配信するアプリと一致させる必要があります。
- 通知プラグイン ネイティブプロジェクトがビルド時に必要な設定を取得することを保証します。
- EAS プロジェクト ID トークン取得の際にアプリが正しい Expo プロジェクトと関連付けられていることを期待している場合に重要です。
通知ハンドラーを早期に設定する
基本的なチュートリアルでは、通知の動作を定義するのを遅すぎます。なぜなら、foreground の動作が予測可能になるからです。アプリ起動時に近く設定してください。
import * as Notifications from 'expo-notifications';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: false,
shouldSetBadge: true,
}),
});
チームは、foreground の通知がアラートを表示するか、サウンドを再生するか、バッジを変更するかを決定します。製品によっては、動作が異なります。チャットアプリと決済アプリは同じ選択肢を選択しません。
foreground の動作を意図的に定義しないと、チームは「見えにくい」通知をデバッグすることになりますが、実際には受信された通知が製品の期待どおりに表示されていませんでした。
Android ではチャンネル設定が必要です。
Android の通知チャンネルは実用上は必須です。チャンネルを省略すると、アラートが不一致またはユーザーの期待に合わないようになります。
import { Platform } from 'react-native';
import * as Notifications from 'expo-notifications';
export async function configureAndroidNotifications() {
if (Platform.OS !== 'android') return;
await Notifications.setNotificationChannelAsync('default', {
name: 'Default',
importance: Notifications.AndroidImportance.MAX,
});
}
アプリ起動時にこの設定を実行し、チャンネル ID を安定させてください。チャンネル ID を頻繁に変更すると、通知の動作を推論することが難しくなります。
パーミッションの要求とプッシュ トークンのキャプチャ
多くのチームはスニペットからコピーし、最終的には後悔する部分です。
パーミッションの要求にはタイミング、プラットフォームの認識、厳格な非同期ハンドリングが必要です。トークン キャプチャは、物理デバイス上で実行され、パーミッションが解決された後、バックエンドに結果を保存する準備ができた場合にのみ行われます。

クライアント関数が基本的な基準
このような関数を使用して、基準点として始めましょう。
import * as Device from 'expo-device';
import * as Notifications from 'expo-notifications';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
type RegisterResult =
| { ok: true; token: string }
| { ok: false; reason: string };
export async function registerForExpoPushNotificationsAsync(): Promise<RegisterResult> {
if (!Device.isDevice) {
return { ok: false, reason: 'Push notifications require a physical device.' };
}
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'Default',
importance: Notifications.AndroidImportance.MAX,
});
}
const permissions = await Notifications.getPermissionsAsync();
let finalStatus = permissions.status;
if (finalStatus !== 'granted') {
const request = await Notifications.requestPermissionsAsync();
finalStatus = request.status;
}
if (finalStatus !== 'granted') {
return { ok: false, reason: 'Notification permission was not granted.' };
}
const projectId =
Constants.expoConfig?.extra?.eas?.projectId ??
Constants.easConfig?.projectId;
if (!projectId) {
return { ok: false, reason: 'Missing EAS project ID configuration.' };
}
const tokenResponse = await Notifications.getExpoPushTokenAsync({ projectId });
return { ok: true, token: tokenResponse.data };
}
順序は重要です。デバイスの種類を確認し、Android チャネル動作を設定し、パーミッションを解決し、プロジェクトの構成を検証し、最後にExpo トークンを要求します。
なぜ Device.isDevice は省略できない
これは、見た目は無害に見えるが、多くのノイズを生み出す一部の間違いです。専門チームは Device.isDevice が真実の場合にのみパーミッションを要求し、は省略できない条件です。開発者が通知を無効のシミュレータ トークンに送信し、Expo に問題があると非難するのは、実際にはアプリの構成に関する説明された問題であるため、 EagerworksのExpo通知実装ノート.
なぜチェックは関数の先頭に置かれているのか。ヘルパーを隠すのではなく、明らかにする
シミュレータの結果はUIテストに役立ちますが、プッシュトークン登録の検証には信頼できません
許可を求める時期を選ぶ
スプラッシュ画面では許可を求めない。ユーザーが価値を理解する前に許可を求めない。通常、ユーザーがアクションを実行した後、通知の利点が具体化される時点で許可を求めるのが最も良い
良い実装はこのフローの流れを遂行する
- ユーザーが意味のある機能境界に到達する
- アプリが通知の価値を独自のUIで説明する
- アプリがシステムの許可を求める
- アプリが許可が付与された場合、トークンをバックエンドに即座に保存する
最後のステップで多くのアプリが失敗する。トークンを取得し、ローカルにログし、バックエンドの登録を延期する。後でサポートは、どのデバイスがどのトークンをどの時点で持っていたのかを判断できない
トークンの保存の簡単な例
export async function enablePushForCurrentUser(userId: string) {
const result = await registerForExpoPushNotificationsAsync();
if (!result.ok) {
return result;
}
await fetch('https://api.example.com/push-tokens', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer user-session-token',
},
body: JSON.stringify({
userId,
token: result.token,
platform: Platform.OS,
}),
});
return result;
}
後続のワークフローでは、このガイドは、視覚的な参照として役立ちます:
リリース重視のアプリを開発するチームにとって、トークン登録をアプリの運用状態の一部として考えることも役立ちます。オンボーディングの一部としてだけではなくて、 より広いExpoアプリ配信ワークフロー
では、アプリの挙動が頻繁に変わり、バックエンドの状態が同期する必要があるためです。
サーバーから通知を送信する
バックエンドに有効なExpo Push Tokenが得られたら、通知を送信することは簡単です。リクエスト自体が難しいことではありません。 fetch:
type ExpoPushMessage = {
to: string;
title: string;
body: string;
sound?: 'default' | null;
data?: Record<string, unknown>;
};
export async function sendExpoPushNotification(token: string) {
const message: ExpoPushMessage = {
to: token,
title: 'New review received',
body: 'Tap to open the order details.',
sound: 'default',
data: {
type: 'new_review',
orderId: 'ord_123',
screen: 'OrderDetails',
},
};
const response = await fetch('https://exp.host/--/api/v2/push/send', {
method: 'POST',
headers: {
Accept: 'application/json',
'Accept-encoding': 'gzip, deflate',
'Content-Type': 'application/json',
},
body: JSON.stringify(message),
});
const result = await response.json();
return result;
}
payloadの内容とクライアントの状態に置ける信頼の度合いを決めるのが難しいことです。
Nodeスタイルの最小限の例
| payloadの各フィールドの役割 | payloadを捨て場として扱わないでください。各フィールドが意図的に使われるようにしてください。 | フィールド |
|---|---|---|
to |
ターゲットエクスポッシュトークン | 現在のデバイスレコードに属することを検証する |
title |
通知のタイトル | 短く人間が読みやすいようにする |
body |
主な表示テキスト | アクションを明確にする |
sound |
システムサウンドの動作 | 高価値のアラートにのみ使用する |
data |
アプリ固有のメタデータ | IDやルートヒントを使用することを推奨する |
この data オブジェクトは、製品ワークフローで役に立つ場所です。タイプとレコードIDを渡し、ユーザーがタップすると最新のデータを取得することができます。 そのほうが、ペイロードに大きなまたは敏感なブロブを直接埋め込むよりも安全です。
ペイロードを小さく、面白くないでください。
Capgoによると Expoの通知のためのCourierのガイド, Expo Push Tokensは、ephemeralとして扱われるべきであり、約 4 KB を超えるペイロードは、ドロップされる可能性があり、信頼できるパターンは、小さなメタデータペイロードを送信することです。たとえば、 { "type": "new_review", "id": 123 } の代わりに、大きなJSONまたはインラインメディアを送信するのではなく。
実際のシステムでは、機能するアドバイスです。小さなペイロードは、より少ない失敗率と、アプリロジックが変更されたときに長く生き残ります。
ユーザーをルーティングするのに十分なデータを送信してください。アプリが開いたら、残りのデータを取得してください。
有用なサーバーサイドの習慣
- テスト用の基本的な送信関数は十分ですが、実際のものは、さらにいくつかの責任を追加します: 送信試行を永続化してください:
- コンテンツ生成とトランスポートを分離する: メッセージコピーを1層で作成し、Expo API リクエストを別の層で作成する。
- 無効化フィードバックを処理する: Expoが後で報告した場合
DeviceNotRegisteredマークするトークンを古いものとし、無視して再試行しない。 - Webhookに適した設計を使用する: システムが既にイベントを発行している場合、同じ種類の バックエンドWebhook処理パターンを使用して通知トリガーをルーティングする。 クライアントリスナをデバッグする前に、手動テストプッシュを送信する。トークンが小さなペイロードを持つ平文通知を受け取った場合、トランスポートパスは正常である可能性があります。そうでない場合、ナビゲーション__CAPGO_KEEP_0__を変更するのではなく、トークン、ペイロードの形状、および許可状態を検証することから始める。
Before debugging client listeners, send a manual test push first. If a token receives a plain notification with a tiny payload, your transport path is probably healthy. If not, don’t start by changing navigation code. Start by validating the token, payload shape, and permission state.
配信は機能の半分だけです。通知が到着したときとユーザーがタップしたときに、アプリが意味のあることを行う必要があります。
__CAPGO_KEEP_0__
その意味は、2つの別々の時点を扱うことです:
- アプリが開いているときに通知が届く
- システムトレイまたはロック画面からユーザーが通知に触れる

フォアグラウンド受信とユーザーからのレスポンスは異なるイベントです。
信頼できるセットアップには両方のリスナーが含まれます:
import { useEffect } from 'react';
import * as Notifications from 'expo-notifications';
export function useNotificationObservers(
onForegroundMessage: (notification: Notifications.Notification) => void,
onNotificationTap: (response: Notifications.NotificationResponse) => void
) {
useEffect(() => {
const receivedSub = Notifications.addNotificationReceivedListener(
(notification) => {
onForegroundMessage(notification);
}
);
const responseSub = Notifications.addNotificationResponseReceivedListener(
(response) => {
onNotificationTap(response);
}
);
return () => {
receivedSub.remove();
responseSub.remove();
};
}, [onForegroundMessage, onNotificationTap]);
}
addNotificationReceivedListener アプリがアクティブなときに実行される。 addNotificationResponseReceivedListener ユーザーが配信された通知をタップしたときに実行される。組み合わせて考えるのではなく、異なるUXパスを提供するため。
データペイロードを読み取り、意図的にナビゲートする。
タップハンドリングの実践的なパターンについてはこちらです。
type NotificationData = {
type?: string;
orderId?: string;
screen?: string;
};
export function handleNotificationTap(
response: Notifications.NotificationResponse,
navigation: any
) {
const data =
response.notification.request.content.data as NotificationData;
if (data.screen === 'OrderDetails' && data.orderId) {
navigation.navigate('OrderDetails', { orderId: data.orderId });
return;
}
if (data.type === 'new_review') {
navigation.navigate('Inbox');
return;
}
navigation.navigate('Home');
}
このパターンは、ペイロードにルーティングヒントが含まれているため、信頼性が高くなります。通知が送信されたときの順序が変更されている場合、アプリはナビゲーション後に現在のサーバー状態を取得できます。
タップされた通知は、明確な目的地に到達するはずです。フォールバックパスが曖昧な場合、ユーザーはすぐに気づきます。
フォアグラウンドの動作はユーザー コンテキストに合わせるべきです。
アプリがすでに開いている場合、システムスタイルのアラートを無視して表示すると、不快な印象を与える可能性があります。時々、適切なアクションはインアプリバナー、バッジの更新、または静的なリフレッシュです。サポートのインボックス画面では、ユーザーがすでにその会話を読んでいる場合、可視的なアラートが必要ないかもしれません。
そのため、フォアグラウンドリスナーはルートと通知タイプによってbranchするべきです。たとえば:
- チャット画面が開いている場合: メッセージを追加し、冗長なバナーの表示を避けます。
- ダッシュボードが開いている場合: 軽量なインアプリトーストを表示します。
- 重要なアカウントイベントの場合: 強力なUIの処理を表面化します。
シンプルなアプローチは次のようになります:
export function handleForegroundNotification(
notification: Notifications.Notification,
currentRouteName: string
) {
const data = notification.request.content.data as { type?: string };
if (currentRouteName === 'ChatThread' && data.type === 'new_message') {
// refresh local thread state
return;
}
// otherwise show your own in-app UI or update badges
}
アプリがこれらのコンテキストを区別しない場合、ユーザーは通知疲れを感じる速度が早くなります。即ち、技術的に正しい通知の配信でもあります。
生産性の向上と一般的な誤り
Expo のプッシュ通知設定が壊れている多くのチームは、Expo が機能が限られているためではない。 それらは、トークンは永久的であると仮定し、ペイロードは何でも運ぶことができ、更新アプリが通知ロジックに影響を与えないと仮定しているため、失敗する。
その仮定は、実際の運用環境では生き残れない。

トークンは、アイデンティティレコードではなく、時期の経過とともに変化するものである。
Expo Push Token は、ライフタイムのデバイス識別子としてではなく、lease として扱うべきである。トークンは再インストール、OS の変更、またはライフサイクルイベントの後に回転する可能性がある。トークンが最終的に戻ってきた場合 DeviceNotRegistered、バックエンドはそれを有効とみなさなくてはならない。
実用的なバックエンドモデルは、以下を保存する。
- ユーザーID
- プラットフォーム
- インストールスコープのメタデータ
- 現在のトークン
- 最後に確認されたタイムスタンプ
- 状態は有効、古い、または取り消されたなど
ユーザー情報テーブルにトークンフィールドを1つだけ保存して終わりはしないでください。ユーザーは複数のデバイスを持っており、デバイスの状態は変化します。
リフレッシュ戦略は、ほとんどのチュートリアルが認めていないほど重要です。
公式のエコシステムガイドラインは、実際の運用上のギャップを残しています。既存のExpoプッシュ通知のコンテンツは、App StoreのレビューサイクルとOTAアップデートの間でトークンの有効性を維持する方法を説明していません。 特にライブ変更を実施するチームにとっては、プッシュ通知の信頼性は現在のトークン状態とバックエンドの同期に依存しているため、重要です。これはExpo通知ドキュメントで説明されています。これは、リフレッシュトリガーの設計に影響を与えるため、注意してください。トークンの状態を整合させる良いタイミングは次のとおりです。 アップデート後のアプリ起動.
ユーザーログイン
- パーミッション設定の変更
- クレデンシャルローテーションの作業
- リリースプロセス
- __CAPGO_KEEP_0__
- 復旧フローはプッシュ関連のサポートチケットの後
セキュリティとコンプライアンスはスプリントの終わりではなく
多くのExpoチュートリアルはメカニズムに焦点を当て、運用上のリスクを省略しています。それは趣味アプリ用に問題ありませんが、医療関連、金融技術、規制商業製品では問題があります。
CourierのExpo通知のギャップに関する企業向けの議論 は、consentログ、監査トレイル、敏感データのペイロード露出を最小限に抑える実践的なガイダンスの欠如を強調しています。直接的なエンジニアリングの取り組みは単純です。
- 通知テキストまたはペイロードメタデータに敏感なビジネスデータを含めないでください。
- consentの変更をサーバー側でログ化してください。
- どのトークンにどの通知の意図が送信されたかを記録してください。
- ペイロードにIDを使用し、アプリを開いた後は保護されたコンテンツを取得してください。
リリースオペレーションをより広範な アプリストアのコンプライアンスとAPI セキュリティの実践と同期するチームではプッシュは、認証、分析、バックエンドイベントログと同じレビューの範囲に含められます。
Push通知はユーザー向けのメッセージですが、分散システムの問題でもあります。 認証状態や支払イベントと同じ注意を払ってください。
Whatが通常機能し、通常機能しないこと
| 通常機能する | 通常機能しない |
|---|---|
| 許可の申請後、明確な値の説明 | 最初のフレームでプロンプトする |
| 実機でのテスト | シミュレータの登録を信頼する |
| デバイスのコンテキストとトークンを保存する | 1つのユーザー記録あたり1つのトークン |
| 小さなメタデータのペイロードを送信する | 大きなまたは敏感なブロブを埋め込む |
| フロントグラウンドとタップイベントを個別に処理する | すべての通知が一つのパスを遷移することを前提としている |
| 古いトークンを積極的に期限切れにする | 死んだトークンを永久にリトライする |
実際には、Capgoの推奨設定では、複雑な設定は必要ない。Disciplinedである。
チームが頻繁にアプリロジックの変更をリリースし、リリースの動作、ロールバック、配信の可視性をより制御したい場合、Capgoはおすすめです。 Capgo Martin Donadieu