◉RORK LABEN
●EXPO — EAS Observe がネイティブクラッシュも記録(10/07)。SDK 57 は 57.0.21 以降で確認できます●SDK 58 — SDK 58 Beta は 9/15 公開。stable の日付はまだ未確認です●RN 0.88 — React Native 0.88.x の正式リリース予定は 10/12、残り3日●Q&A — expo-widgets が本番ビルドだけ真っ黒になる、という問いが出ています●RORK — 09-29 に GPT-6.1 Sol を追加。Pro・Max プランで利用できます●NEW — クライアントのアプリを作る前に決める、公開アカウントの持ち主●EXPO — EAS Observe がネイティブクラッシュも記録(10/07)。SDK 57 は 57.0.21 以降で確認できます●SDK 58 — SDK 58 Beta は 9/15 公開。stable の日付はまだ未確認です●RN 0.88 — React Native 0.88.x の正式リリース予定は 10/12、残り3日●Q&A — expo-widgets が本番ビルドだけ真っ黒になる、という問いが出ています●RORK — 09-29 に GPT-6.1 Sol を追加。Pro・Max プランで利用できます●NEW — クライアントのアプリを作る前に決める、公開アカウントの持ち主
記事一覧/開発ツール
⬡ 開発ツール/2026-07-07中級

アイコンのバッジが「3」のまま消えない — Expoアプリのバッジ数をSource of Truthで作り直す

Expoアプリのアイコンバッジが実際の未読と食い違い、消えなくなる原因を突き止め、バッジ数を状態から再計算して同期するSource of Truth設計を、動くコードと実運用の落とし穴つきで整理します。

Expo213React Native238通知4バッジRork577

✦ プレミアム記事

リリースから数週間後、レビュー欄に「アイコンの赤いバッジが3のまま、アプリを開いても消えない」という報告が並びました。私が個人開発で運営している習慣化アプリでの出来事です。中を開けば未読は0件なのに、ホーム画面のアイコンだけが「3」を主張し続ける。ユーザーからすれば、アプリが嘘をついているように見えます。

原因を追ってみると、コードのどこにも「バッジを3にする」とは書いていませんでした。バッジを増やしていたのは通知そのもので、それを減らす責任を誰も持っていなかったのです。以下では、その食い違いがなぜ起きるのかを分解し、バッジ数を状態から再計算して同期する設計へ作り直すまでを、動くコードとともに追っていきます。

バッジが「ズレる」本当の原因

Expo(React Native)アプリのバッジ数は、実は二つの主体が別々に書き込んでいます。

一つは通知システムです。ローカル通知やプッシュ通知に badge の値が含まれていると、OSが受信時にアイコンバッジを自動で書き換えます。もう一つはアプリ本体で、Notifications.setBadgeCountAsync() を呼んだときだけ書き換わります。

問題は、この二つが互いの存在を知らないことです。通知が届くたびにOSがバッジを1つ増やしても、ユーザーがアプリ内でその項目を読んだとき、アプリ側は「バッジを減らす」処理を明示的に呼ばない限り何もしません。結果として、通知が積み上げた数値だけが取り残されます。

さらにやっかいなのは、通知を配信したときの badge 値が「その通知が来た瞬間の未読数」で固定される点です。ユーザーが通知を無視している間に状態が変わっても、過去に予約した通知の badge は古い数値のまま発火します。バッジは「今の未読数」ではなく「予約した時点のスナップショット」を映してしまうのです。

Source of Truth を1つに決める

この問題は、バッジ数を「誰が書き込むか」で考えている限り解けません。書き込む主体が複数あることが原因だからです。

発想を変えて、バッジ数は「アプリの状態から一意に計算されるもの」と定義し直します。未読の通知、未完了のタスク、確認待ちのリマインダー——アプリが持つ状態のうち、バッジに反映すべきものを1本の関数にまとめ、その関数だけがバッジの真実を決めます。通知の badge フィールドには一切頼りません。

私はこの方針を、以前 AdMob の広告解放状態を扱ったときと同じ考え方で組みました。複数の判定材料があるものは、必ず1本の合成関数を経由させ、そこ以外では状態を組み立てない。バッジもまったく同じ構造で扱えます。

まず、バッジに寄与する状態を集めて数値を返す純粋関数を用意します。

// badge/computeBadgeCount.ts
// アプリの状態からバッジ数を「計算」する唯一の関数。
// ここ以外の場所でバッジ数を組み立ててはいけない。
 
import type { AppState } from "../store/types";
 
export function computeBadgeCount(state: AppState): number {
  // バッジに反映したいものだけをここで合算する。
  // 例: 未読のお知らせ + 期限切れの未完了リマインダー
  const unreadNotices = state.notices.filter((n) => !n.read).length;
  const dueReminders = state.reminders.filter(
    (r) => !r.completed && r.dueAt <= Date.now()
  ).length;
 
  // 合計が負やNaNにならないよう最後に正規化する
  const total = unreadNotices + dueReminders;
  return Number.isFinite(total) && total > 0 ? total : 0;
}

この関数はストアの状態だけを入力に取り、副作用を持ちません。テストも書きやすく、「バッジが何を意味するか」がコード1箇所に集約されます。

✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦アイコンバッジが「3」のまま消えないユーザー報告に悩んでいた人が、その日のうちに再計算式の同期コードを手に入れられる
✦通知が設定するバッジとアプリが設定するバッジの二重管理をやめ、状態から一意に決まる1本の関数へ統合できる
✦フォアグラウンド復帰・通知受信・iOS/Androidの挙動差まで含めて、バッジが二度とズレない再同期設計を実装できる
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

関連記事

⬡ 開発ツール2026-09-04
EAS の secret は「アプリに入れない」設定ではありません — 接頭辞と可視性を別々に決める
EXPO_PUBLIC_ の接頭辞は成果物に入るかを決め、EAS の可視性は誰が読めるかを決めます。二つを重ねたときに OTA 更新でだけ値が消える筋道と、出荷前に自分の手で確かめる手順をまとめました。
⬡ 開発ツール2026-08-22
一括置換は全ファイル成功しました。壊れたのは、消さなかった行のほうです
生成コードに一括置換をかけると、一致した行ではなく隣の行が壊れます。運用中のプロジェクトで実際に起きた破損と、置換の不変条件を検査する20行のガードスクリプトをまとめました。
⬡ 開発ツール2026-07-30
Renovate が触ってよい依存と、触ってはいけない依存 — Expo SDK 57 の123パッケージを機械的に切り分ける
Rork 生成アプリに依存の自動更新を入れると、Expo が版を管理している123パッケージまで巻き込まれます。2026年7月30日時点の実測では、うち6件が npm の最新版へメジャー更新される差でした。除外リストを手で書かず SDK から機械生成する手順をまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます