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

正しくなされたライブラリと開発環境を始めましょう。エクスポーゴを超えて作業している場合、ローカルワークフローをカスタムエクスポー開発クライアント設定と合わせることが役立ちます。 カスタムエクスポー開発クライアント設定、なぜなら、通知の動作を生産環境に近いビルドで検証する必要があるからです。
通知パッケージをインストールします。
最低限、許可要求、トークン取得、リスナー、通知表示が必要です。
expo-notificationsなぜなら、トークン取得を守る必要があるからです。expo-devicetokenの取得を守るために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"
}
}
}
}
ここではいくつかの詳細が重要です。
- Bundle IDとパッケージ名 実際に配信するアプリと一致する必要がある。
- ノーティフィケーション プラグイン ネイティブプロジェクトの設定がビルド時に必要なものであることを保証する。
- EAS プロジェクト ID トークン取得の際に、正しいExpoプロジェクトと関連付けられていることをアプリが想定している場合に重要である。
通知ハンドラーを早めに設定する
基本的なチュートリアルでは、通知の挙動を定義するのが遅すぎることが多い。なぜなら、通知の挙動を予測できるように、起動時近くに定義する必要があるからだ。
import * as Notifications from 'expo-notifications';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: false,
shouldSetBadge: true,
}),
});
チームは、前面通知がアラートを表示するか、音を鳴らすか、バッジを変更するか、どの挙動を選択するかを決定する。挙動は製品によって異なる。チャットアプリと決済アプリは同じ決定を下すことはできない。
挙動を意図的に定義しないと、チームは「見えていない」通知をデバッグすることになる。実際には受信されたものの、製品が想定していたように表示されなかったからだ。
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 true許可を求める条件が満たされている場合、 EagerworksのExpo通知実装ノート.
許可を求める条件を隠すのではなく、明確に表示してください。
シミュレータの結果は、UIテストに役立ちますが、プッシュトークンの登録を検証するには信頼できません。
許可を求めるタイミングを考慮してください。
コンテキスト: Capgo マーケティング ウェブサイト。役割: コール トゥ アクション ボタンまたはリンク ラベル。見られる場所: コンポーネント AskAiSection.astro。メッセージ キー `ask_ai_button` (Ask Ai Button)。
良い実装は通常この流れを遂行します。
- 良い実装では、以下の流れを遂行します。
- ユーザーが意味のある機能境界に到達します。
- アプリが通知の価値をユーザー自身の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;
}
後続のワークフローでは、このガイドは、トークンを保存する方法を視覚的に理解するのに役立ちます。
リリース頻繁なアプリを構築するチームにとって、トークン登録をアプリの運用状態の一部として考えることも役立ちます。アプリのオンボーディングの一部だけではありません。そうした考え方は、より広範なエクスポアプリ配信ワークフローと相性が良くなります。 サーバーから通知を送信するバックエンドに有効なエクスポプッシュトークンが存在する場合、通知を送信することは簡単です。要求自体が難しいことではありません。ペイロードに何が含まれるか、クライアントの状態にどれだけの信頼を置くかを決定するのが難しいことです。
Nodeスタイルの最小限の例
ペイロードの各フィールドの役割
ペイロードを捨て場として扱わないでください。各フィールドが意図的な役割を果たすようにしてください。 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;
}
送信する通知のペイロードのフィールドの役割を理解する
通知のペイロードのフィールドを意図的に使用する
| Field | 目的 | 実践的なアドバイス |
|---|---|---|
to |
対象Expo Push Token | 現在のデバイスレコードに属していることを検証する |
title |
通知タイトル | 短く人間が読みやすい |
body |
主な表示テキスト | アクションを明確にする |
sound |
システムサウンドの挙動 | 高価値のアラートにのみ使用する |
data |
アプリ固有のメタデータ | IDとルートヒントを優先してください |
The data オブジェクトは、製品ワークフローが役に立つ場所です。型とレコードIDを渡すことができ、ユーザーがタップすると最新のデータを取得できます。そのほうが安全です。直接ペイロードに大きなまたは敏感なブロブを埋め込むのではなく。
ペイロードを小さくして面白くしないでください
CourierのExpo通知ガイドによると エクスポの通知ガイド大きいJSONやインラインメディアではなく、 4 KB ユーザーをルーティングするのに十分なデータを送信してください。アプリが開いたら、残りのデータを取得してください。 { "type": "new_review", "id": 123 } サーバーサイドの習慣
ユーザーに十分なデータを送信して、ルーティングを実行する。アプリが起動した後、残りのデータを取得する。
サーバーサイドの良い習慣を身に付ける
A production one is fine for testing. A production one usually adds a few more responsibilities:
- 送信試行を保存する: ユーザーID、トークン、ペイロードタイプ、タイムスタンプと共に通知の意図を保存する。
- コンテンツ生成と送信を分離する: メッセージのコピーを1つのレイヤーで作成し、Expo API リクエストを別のレイヤーで作成する。
- 無効化フィードバックを処理する: Expoが後で報告した場合
DeviceNotRegisteredマークしてトークンを古くし、無視して再試行を止める。 - Webhookに適した設計を使用する: システムが既にイベントを発行している場合、同じ種類のバックエンドWebhook処理パターンを使用して通知トリガーをルーティングする。 backend webhook処理パターン あなたが他の場所で使用している
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.
アプリ内で受け取った通知を処理する
配信は機能の半分だけです。アプリは通知が届いたときとユーザーが通知をタップしたときに、意味のあることを行う必要があります。
つまり、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');
}
このパターンは、ルーティングヒントが含まれるパayloadを使用するため、変更された順序を検知することができます。通知が送信された時点でのサーバーの状態を取得することで、現在の状態を取得できます。
タップされた通知は、明確な目的地に到達するようにする必要があります。フォールバックパスが曖昧な場合、ユーザーはすぐに気づきます。
フォアグラウンドの動作はユーザーの状況に合わせる必要があります。
アプリがすでに開いている場合、システムスタイルのアラートを表示するだけでは、不快な印象を与える可能性があります。場合によっては、インアプリバナー、バッジの更新、または静的なリフレッシュが適切な動作となる場合があります。サポートのインボックス画面では、ユーザーがすでにその会話を読んでいる場合、可視的なアラートは必要ありません。
そのため、フォアグラウンドリスナーはルートと通知タイプに基づいて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
- プラットフォーム
- platform
- インストールスコープのメタデータ
- 現在のトークン
- 最後の見つかったタイムスタンプ
- ステータス(有効、古い、または取り消された)
ユーザー表に1つのトークンフィールドを保存して終わりはしないでください。ユーザーは複数のデバイスを持っており、デバイスの状態は変化します。
リフレッシュ戦略は、ほとんどのチュートリアルが認めるよりも重要です。
公式のエコシステムガイドラインは、実際の運用上のギャップを残しています。既存のExpoプッシュ通知のコンテンツは、App StoreのレビューサイクルとOTAの更新を通じてトークンの有効性を維持する方法を説明していません。 アプリストアレビューサイクルとOTA更新これは、リフレッシュトリガーの設計に影響を与えることです。トークンの状態を整合させる良いタイミングには次のものがあります。 アップデート後のアプリ起動.
App Storeのレビューサイクル
- OTAの更新
- ユーザー認証
- 許可設定の変更
- クレデンシャルローテーションがリリースプロセスに
- プッシュ関連のサポートチケット後の復旧フロー
セキュリティとコンプライアンスはスプリントの終わりではなく
多くのExpoチュートリアルはメカニズムに焦点を当て、運用上のリスクを省略します。趣味アプリ用には問題ありません。が、医療関連、金融技術、規制商取引製品では問題があります。
CourierのExpo通知のギャップに関するエンタープライズ向け議論 は、consentログ、監査トレイル、敏感なペイロードの露出を最小限に抑える実践的なガイダンスの欠如を強調しています。直接的なエンジニアリングの取り組みは単純です:
- 通知テキストまたはペイロードメタデータに敏感なビジネスデータを含めないでください。
- consentの変更をサーバー側でログしてください。
- どのトークンにどの通知の意図が送信されたかを記録してください。
- ペイロードにIDを使用し、アプリを開いた後、保護されたコンテンツを取得してください。
リリース作業をより広範なアプリストアの規制とセキュリティ慣行と一致させるチーム向けに API セキュリティ慣行と同様のレビューの範囲にプッシュを含めるプッシュはユーザー向けのメッセージですが、auth、分析、バックエンドイベントロギングと同様に分散システムの問題として扱う必要があります。
プッシュ通知はユーザー向けのメッセージですが、auth状態と支払イベントと同様に扱う必要があります。
通常は機能し、通常は機能しません
| 通常は機能しません | 明確な値の説明の後に許可を求める |
|---|---|
| 最初のフレームで提示する | 実機でテストする |
| シミュレータの登録を信頼する | デバイスのコンテキストとともにトークンを保存する |
| 通常は機能し、通常は機能しません | 各ユーザーレコードごとに1つのトークン |
| 小さなメタデータペイロードを送信 | 大きなまたは敏感なブロブを埋め込む |
| 前景とタップイベントを別々に処理する | すべての通知が1つのパスを遷移することを前提とする |
| 古いトークンを積極的に期限切れにする | 死んだトークンを永遠にリトライする |
良いエクスポ推奨設定は複雑ではない。 それは規律である。
あなたのチームが頻繁にアプリロジックの変更を実行し、リリースの動作、ロールバック、配信の可視性についてより厳密に制御したい場合、 Capgo はおすすめです。 これは、通知フローの、ルーティングロジック、またはクライアントサイドの修正がユーザーに迅速に到達する必要がある場合に、ストアのレビューを待つことなく、モバイルチームが更新を迅速に送信できるように支援します。