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

正しいライブラリがインストールされていることと、ビルドパスに合った開発環境から始める。Expo Goを超えて作業している場合は、ローカルワークフローをカスタムExpo開発クライアント設定と合わせることが役に立つ __CAPGO_KEEP_0__, 通知の動作がビルドで検証される必要がある場合、生産環境に近いものが必要です。
通知パッケージをインストールする
最低限、通常は必要なのは:
expo-notifications許可の要求、トークンの取得、リスナー、通知の表示のためにexpo-deviceトークンの取得を守るためにDevice.isDevice.
通常のインストールコマンドはパッケージマネージャーに依存しますが、主なのはエクスポのバージョンと一致させることです。SDK。エクスポが互換性のあるバージョンを解決するようにしましょう。
プロジェクトのレベルで設定を追加する
設定を明示的に行うことが重要です。最小限の app.json または 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"
}
}
}
}
いくつかの詳細は重要です:
- バンドル識別子とパッケージ名 __CAPGO_KEEP_0__
- 通知プラグイン ネイティブプロジェクトの設定がビルド時に必要なものが得られるようにする
- ExpoプロジェクトID アプリが正しく関連付けられていることを確認するために、トークン取得が期待している場合に重要です。
起動時早く通知ハンドラーを設定する
基本的なチュートリアルでは、通知の動作を定義するのが遅すぎることが多い。
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を安定させる。
Requesting Permission and Capturing Push Tokens
This is the part many teams copy from a snippet, then eventually regret.
Permission requests need timing, platform awareness, and disciplined async handling. Token capture needs to happen only on a physical device, only after permissions are resolved, and only if you’re ready to store the result on your backend immediately.

The client function that should be your baseline
Use a function like this as your starting point:
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 };
}
The order matters. You check device type first, set Android channel behavior, resolve permissions, validate project configuration, then request the Expo token.
Why Device.isDevice isn’t optional
This is one of the few mistakes that creates a lot of noise while looking harmless. Expert teams only request permission conditionally when Device.isDevice is true, and a common pitfall is skipping that guard, which leads developers to send notifications to invalid simulator tokens and blame Expo when the issue is really app configuration, as described in 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が得られたら、通知を送信することは簡単です。リクエスト自体が難しい部分ではありません。payloadの内容とクライアントの状態に置ける信頼の度合いが難しい部分です。
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;
}
payloadの各フィールドが何を実現するか
payloadを捨て場として扱わないでください。各フィールドが意図的なものでなければなりません。
| フィールド | 目的 | 実践的なアドバイス |
|---|---|---|
to |
ターゲットエクスポッシュトークン | __CAPGO_KEEP_0__を現在のデバイスレコードに属していることを検証する |
title |
通知ヘッダー | 短く人間が読みやすいようにしてください |
body |
メインの可視テキスト | アクションを明確にする |
sound |
システムサウンドの動作 | 高価なアラートの場合のみ、sparingly使用してください |
data |
アプリ固有のメタデータ | IDとルートヒントを優先して、リッチなコンテンツを避けてください |
その data オブジェクトは、製品ワークフローで便利な場所です。タイプとレコードIDを渡すことができ、ユーザーがタップすると最新のデータを取得できます。そのほうが安全です。直接ペイロードに大きなまたは敏感なブロブを埋め込むのではなく。
__CAPGO_KEEP_0__を小さく、面白くないように保管
__CAPGO_KEEP_0__に従って エクスポの通知のためのカウティヤーのガイドエクスポのプッシュトークンは、ephemeralとみなされるべきです。__CAPGO_KEEP_0__のサイズ制限を超えるパイロットは、失われる可能性があり、信頼できるパターンは、小さなメタデータパイロットを送信することです。__CAPGO_KEEP_1__やインラインメディアではなく、大きなJSONです。実際のシステムでは、機能するアドバイスはこれです。小さなパイロットは、より少ない失敗と、アプリロジックが変更されたときに長く生き残ります。 ユーザーをルーティングするのに十分なデータを送信し、開いた後は残りのデータを取得します。 サーバーサイドの習慣 { "type": "new_review", "id": 123 } テスト用の基本的な送信関数は十分ですが、生産用のものは通常、以下の責任を追加します:
送信試行を永続化する:
ユーザーID、トークン、パイロットタイプ、タイムスタンプと共に通知の意図を保存します。
__CAPGO_KEEP_1__
- __CAPGO_KEEP_2__ __CAPGO_KEEP_3__
- コンテンツ生成とトランスポートを分離する: メッセージのコピーを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__
That means handling two separate moments:
- アプリが開いているときに通知が届く時と
- ユーザーがシステムトレイまたはロック画面から通知に反応する時

アプリが前面に表示されている時とバックグラウンドに表示されている時のプッシュ通知ライフサイクル
Foreground receipt and user response are different events
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パスを提供します。
Read the data payload and navigate intentionally
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');
}
通知のデータペイロードを読み取り、意図的に移動する
Here’s a practical pattern for tap handling: This pattern stays resilient because the payload contains routing hints, not whole documents. If the order has changed since the notification was sent, the app can fetch current server state after navigation. A tapped notification should lead to one obvious destination. If your fallback path is vague, users notice immediately.
ユーザー環境に合わせた背景動作
アプリがすでに開いている場合、システムスタイルのアラートを無視して表示すると、不快な印象を与える可能性があります。時々、適切なアクションは、インアプリバナー、バッジの更新、または静的なリフレッシュです。サポートのインボックス画面では、ユーザーがすでにその会話を読んでいる場合、可視的なアラートが必要ないかもしれません。
そのため、フォアグラウンドリスナーはルートと通知タイプによって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は、ライフタイムのデバイスIDとしてではなく、leaseとして扱うべきである。 DeviceNotRegisteredトークンは再インストール、OSの変更、ライフサイクルイベントなどによって回転する可能性がある。トークンが最終的に戻ってきた場合、バックエンドはそれを有効として扱うべきではない。
実用的なバックエンドモデルは、以下の情報を保存する。
- ユーザーID
- プラットフォーム
- インストールスコープのメタデータ
- 現在のトークン
- 最後に確認されたタイムスタンプ
- __CAPGO_KEEP_0__
ユーザー情報にトークンを保存しないでください。ユーザーは複数のデバイスを使用し、デバイスの状態は変化します。
リフレッシュ戦略は、多くのチュートリアルが認めていないほど重要です。
公式のエコシステムガイドラインでは、実際の運用上のギャップが残っています。 App StoreのレビューサイクルとOTA更新で、トークンの有効性を維持することが難しくなります。これは、ライブ変更を配信するチームにとって特に重要です。プッシュ通知の信頼性は、トークンの現在の状態とバックエンドの同期に依存しているため、 Expo通知ドキュメント.
これは、リフレッシュトリガーの設計に影響を与えるため、注意が必要です。トークンの状態を整合させる良いタイミングは次のとおりです。
- アプリの更新後に起動
- ユーザーがサインイン
- パーミッション設定の変更
- 資格情報のローテーション作業をリリースプロセスに組み込んでください
- プッシュ関連のサポートチケットの後、復旧フロー
スプリントの最後にセキュリティとコンプライアンスは属さない
エキスポのチュートリアルは、多くはメカニズムに焦点を当て、運用上のリスクを省略している。趣味用のアプリでは問題ないが、医療関連、金融技術、規制された商業製品では問題がある。
エキスポの通知のギャップについてのエンタープライズ向けの議論 エキスポの通知のギャップについてのエンタープライズ向けの議論
- エキスポの通知のギャップについてのエンタープライズ向けの議論
- エキスポの通知のギャップについてのエンタープライズ向けの議論
- エキスポの通知のギャップについてのエンタープライズ向けの議論
- エキスポの通知のギャップについてのエンタープライズ向けの議論
エキスポの通知のギャップについてのエンタープライズ向けの議論 app store compliance and API security practicesエキスポの通知のギャップについてのエンタープライズ向けの議論は、実用的なガイダンスが不足していることを強調している。直接的なエンジニアリングの取り組みは単純である。
__CAPGO_KEEP_0__はユーザーフェイスのメッセージですが、分散システムの問題でもあります。認証状態や支払イベントと同じ注意を払ってください。
__CAPGO_KEEP_0__はどれが機能し、どれが機能しないか
| 機能する | 機能しない |
|---|---|
| 許可を求めるには、明確な値の説明が必要です | 最初のフレームで提示する |
| 実機でテストする | シミュレータの登録を信頼する |
| デバイスのコンテキストとトークンを保存する | ユーザー レコードごとに 1 つのトークン |
| 小さいメタデータ ペイロードを送信する | 大きなまたは敏感なブロブを埋め込む |
| 前景とタップイベントを別々に処理する | すべての通知が一つのパスを遷移することを前提とする |
| 古いトークンを積極的に期限切れにする | 死んだトークンを永久にリトライする |
__CAPGO_KEEP_0__
Expo のプッシュ設定は、厳格さが必要です。 Capgo はおすすめです。