OpenAIExpo2026/07/22 13:15

From Expo Router to Detour: Deferred deep linking the right way

要点だけを先に読めるように短く再構成したセクションです。

元記事

Quick Digest

要約

要点だけを先に読めるように短く再構成したセクションです。

openaijamodel: gpt-5-mini-2025-08-07

Expo RouterからDetourへ:適切なDeferred Deep Linking

Key Points

  • インストールギャップを解消
  • ルーター前にリンク解決
  • Androidは決定論的、iOSは確率的

Summary

Expo Routerを使ったモバイルアプリで、ストア経由のインストールによって元のリンク情報が失われる「インストールギャップ」を解消するためのライブラリDetourの紹介。DetourはExpo Routerの拡張点(+native-intent.tsx)に直接フックして、初回レンダー前にリンクを解決・ルーターへ渡します。認証ゲートによるリンクの喪失を防ぐためにDetourProvideruseDetourContext()でリンクを保持し、適切なタイミングで一度だけナビゲーションを再生します。AndroidはInstall Referrerで決定論的にマッチし、iOSは非同一性情報のスナップショットで確率的にマッチします。

Key Points

  • 初回レンダー前にプリルーティングでリンクを捕捉
    • +native-intent.tsxcreateDetourNativeIntentHandler(...)を登録して、アプリ起動時に元のURLを解決してルーターへ渡す。
  • 認証フローと競合しない設計
    • DetourProviderがリンクをメモリに保持し、useDetourContext()isLinkProcessedisSignedInの組み合わせで安全にrouter.replace(link.route)を実行し、clearLink()で一度だけ処理する。
  • プラットフォームごとのマッチング
    • Android: Install Referrer経由でclick_idを渡し1:1で復元(高精度)。
    • iOS: プライバシー境界のため信号のスナップショットで確率的にマッチ。
  • なぜルーター側で処理するか
    • サードパーティSDKの分離したリスナーだと二重遷移やライフサイクル競合が起きやすいため、ルーターの延長点で処理するほうが堅牢。
  • 実装上の注意点(チェックリスト)
    • フォールバックパスとホストマッチングを設定する(例: fallbackPath: "/", hosts: [/\.godetour\.link$/i])。
    • 認証状態を待ってからリンクを適用するロジックを入れる。
    • リンクは処理後に確実にクリアして重複トリガーを防ぐ。

簡潔に言うと、Detourは「ルーターの文脈で」Deferred Deep Linkingを解決することで、認証やライフサイクルの競合を回避し、再現性の高い誘導を実現します。

Full Translation

翻訳

原文の流れを保ったまま読める翻訳セクションです。

openaijamodel: gpt-5-mini-2025-08-07

Expo RouterからDetourへ:正しい方法で行う遅延ディープリンク

Expo RouterからDetourへ:遅延ディープリンクを正しく行う方法

これは Software Mansion の Bartek Krasoń によるゲスト投稿です。現在彼は Detour に取り組んでいます。

もし Expo でモバイルアプリを構築しているなら、ナビゲーションロジックを予測可能かつスケーラブルに保つために Expo Router を使っている可能性が高いでしょう。おそらく既に標準的なディープリンク(iOS の Universal Links、Android の App Links)を設定し、既存ユーザーに対しては期待通り動作しているはずです。しかし、新しいユーザーがプライベート招待リンクや特定商品のリンクをクリックしたとき、アプリをまだインストールしていなかったらどうなるでしょうか?

何が起きているのか(そしてなぜ)

標準のディープリンク(iOS の Universal Links、Android の App Links)は、アプリが既にインストールされている場合は非常にうまく機能します。リンクはアプリを開き、Expo Router は正しい画面へ遷移させます。シンプルです。

しかし新しいユーザーが App Store または Google Play を経由すると、その URL コンテキストは失われます。iOS と Android はストアの境界を越えてリンクデータを保持しないため、アプリはインストールによって起動された元の意図(どの URL をタップしたのか)をまったく認識できません。この摩擦は「Installation Gap(インストールギャップ)」として知られています。

解決法:遅延ディープリンク(Deferred deep linking)

目的は、ユーザーの元の意図(タップした正確な URL、パス、パラメータ)を取得し、それをインストールプロセスを通じて保持し、アプリが初めて起動した瞬間に再生(replay)することです。

既存のオプション

一部のプラットフォームはこの問題を長年解決してきましたが、それらはマーケティングアトリビューションツールが主で、ディープリンクは副次機能です。多くの場合、コールバックで生の URL 文字列を渡され、それを自分でナビゲーションに橋渡しする必要があります。

Expo Router アプリでは、これはライフサイクルのタイミング管理、ナビゲーションイベントの重複、認証ゲートとの競合状態を扱う脆弱なグルーコードを書くことを意味します。SDK がルーターとはまったく別の世界で動作するため、本来存在するべきでない配管(plumbing)を自分で作らなければならなくなります。

Detour のアプローチ

私たちは Software Mansion でこの問題をナビゲーションの側から解決するために Detour を作りました。チームは 2017 年から Expo エコシステム(Reanimated、Gesture Handler、EAS の一部など)に貢献しており、ルーターと統合する遅延ディープリンクツールを望んでいました。コアのアイデアは、Detour が最初の画面がレンダリングされる前にリンクを解決できるよう、Expo Router の拡張ポイントを利用することです。ルーターは最初から意図(intent)を認識しているため、post-mount の useEffect による競合や手動の URL パースが不要になります。

以下は実際の動作例です。

最初のレンダー前にリンクをインターセプトする

Expo Router は +native-intent.tsx ファイルを公開しており、ルーティングが開始される前に着信 URL をインターセプトして変換できます。Detour はこれに直接フックします:

// app/+native-intent.tsx
import { createDetourNativeIntentHandler } from "@swmansion/react-native-detour/expo-router";
export const redirectSystemPath = createDetourNativeIntentHandler({
  fallbackPath: "/",
  hosts: [/\.godetour\.link$/i],
});

これはプリルーティングのミドルウェアとして機能します。アプリが起動すると、Detour はこれが遅延ディープリンクかどうかをチェックし、元の URL を解決して、どの画面もマウントされる前にルーターに渡します。

認証ゲートの処理

一般的な落とし穴が認証ゲートです。ユーザーが招待リンクをタップし、アプリをインストールして開くと、Detour は元の URL を解決します。しかしユーザーはまだログインしていないため、あなたの認証ガードは /login にリダイレクトします。その結果、解決されたリンクは飲み込まれてしまいます。

DetourProvider は、アプリがユーザーの準備ができたことをシグナルするまでリンクの意図をメモリに保持することでこれを解決します:

// app/_layout.tsx
import { DetourProvider, useDetourContext } from "@swmansion/react-native-detour";
export default function RootLayout() {
  return (
    <DetourProvider config={{ appID: "YOUR_APP_ID", apiKey: "YOUR_API_KEY" }}>
      <AuthProtectedStack />
    </DetourProvider>
  );
}

function AuthProtectedStack() {
  const { link, clearLink, isLinkProcessed } = useDetourContext();
  const { isSignedIn } = useAuth(); // Your logic
  const router = useRouter();
  useEffect(() => {
    if (isLinkProcessed && link && isSignedIn) {
      clearLink(); // Ensure the link only fires once
      router.replace(link.route);
    }
  }, [link, isLinkProcessed, isSignedIn]);
  return <Stack />;
}

リンクは isSignedIn が true になるまでメモリに残ります。ユーザーがログインすると(もしくは既にログインしていた場合)、ナビゲーションはちょうど一度だけ発火して自身をクリアするため、重複トリガーや認証リダイレクトとの競合は発生しません。

マッチングは内部でどう動くか

遅延ディープリンクは次の一問に答える必要があります:

「ブラウザで n 秒前にそのリンクをクリックしたのと同じユーザーなのか?」

答えはプラットフォームによって異なります。

  • Android: 決定論的マッチング。Detour は Install Referrer API 経由で Google Play ストアに一意の click_id を渡します。アプリが最初に起動したとき、SDK はその正確な click_id を取得します。これはリファラクリックと最初のアプリ起動の間に直接のデータチャネルがあるため、1:1 の一致で 100% の精度です。

  • iOS: 確率的マッチング。Apple は App Store をプライバシー境界として扱います。Install Referrer API のようなものは存在しません。代わりに、Detour はブラウザクリック時に識別子を含まない信号のスナップショットを取得し、アプリが開いたときに取得する第2のスナップショットと照合します。

ルーターレベルでこれが重要な理由

+native-intent.tsx を通してリンクを処理することで、単一のリンクが複数のナビゲーションを引き起こしたり、初期 URL と競合したりする一般的な不具合を防げます。典型的なサードパーティ SDK では、着信リンクはナビゲーション構造から切り離されたリスナーに到達し、ナビゲーションとの受け渡しを自前で扱う必要があります。その結果、認証ゲートの競合、深いネストでの問題、冗長なトリガーに対処することになります。SDK とルーターは別々の世界にあり続けます。

ルーターは Web からアプリへのユーザージャーニーが移行するまさにその場所です。したがって、Detour の目標はプラットフォームの進化に合わせて常に同期を保ち、ハンドオフが技術的な副作用ではなく UX の一部として意図的に感じられるようにすることです。

今後の展望

Detour 1.0 はカスタムドメインや React Native、Flutter、ネイティブプラットフォーム向け SDK を含むコア要件を確立しました。今後は、より多くのユーザージャーニーをカバーするためにプラットフォームを拡張しつつ、すべてを開発者ファーストで手頃な価格に保ちたいと考えています。

私たちは Detour の開発を積極的に行っており、Expo コミュニティからのフィードバックを歓迎します。問題に遭遇したり機能要望がある場合は、GitHub に issue を開くか Discord で連絡してください!