Expo Router から Detour へ:遅延ディープリンクを正しく実装する方法

標準のディープリンクはストアの境界で機能しなくなる。Software Mansion の Detour が、Expo Router の内部でこのインストールの断絶をどう埋めるのかを見ていく。

日本語
コピー
From Expo Router to Detour: Deferred deep linking the right way

本稿は Bartek Krasoń によるゲスト記事です。同氏は Software Mansion のソフトウェアエンジニアで、現在は 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 も、リンクのデータをアプリストアという境界の向こう側へ持ち越させません。そのためアプリは起動時に、そもそもこのインストールを引き起こした意図について何も知らない状態になります。

この摩擦が「インストールギャップ(Installation Gap)」と呼ばれるものです。ではどう解決するのか。遅延ディープリンク(Deferred deep linking)です。 考え方はこうです。ユーザーが最初に抱いた意図——クリックした URL、パス、パラメータ——を捕捉し、インストールの間ずっと保持しておき、アプリの初回起動時にもう一度再生する。

既存のソリューション

一部のプラットフォームは何年も前からこの問題を解決しています。ただしそれらは何よりもまずマーケティングのアトリビューションツールであり、ディープリンクはあくまで副次的な機能にすぎません。コールバックで生の URL 文字列を渡してくるだけで、それをどうナビゲーションにつなぐかは自分でやるしかありません。

Expo Router のアプリでは、これは脆弱なグルーコードを大量に書くことを意味します。ライフサイクルのタイミング、重複するナビゲーションイベント、認証ガードとの競合——それらに対処するコードです。SDK とあなたのルーターはまったく別の世界に住んでいて、本来存在すべきでない配管を自分で組み立てるしかないのです。

Detour のアプローチ

私たちは Software Mansion で Detour を作りました。この問題をナビゲーションの側から解決するためです。チームは 2017 年から Expo エコシステムに貢献してきました(Reanimated、Gesture Handler、そして EAS の一部の仕事)。私たちが欲しかったのは、ルーターの外側に巻き付くのではなく、ルーターと一体になる遅延ディープリンクのツールでした。

核心となる考え方:Detour が初回レンダリングより前にリンクを解決できるのは、Expo Router 自身の拡張ポイントを利用しているからです。ルーターは最初からこの intent を把握しているため、マウント後に起きる 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 を解析しました。しかしユーザーはまだログインしておらず、あなたの auth guard が彼を /login へリダイレクトし、解析済みのリンクはそのまま飲み込まれてしまいます。

DetourProvider の解決策は、アプリがユーザーの準備完了を通知するまでリンクの intent をメモリに保持することです:

// 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 になるまでメモリに残ります。ユーザーがログインした後(あるいはすでにログイン済みなら)、ナビゲーションは一度だけ発火して自動的にクリアされます。したがって重複して発火することも、auth のリダイレクトと競合することもありません。

マッチングの仕組み

遅延ディープリンクは一つの問いに答える必要があります。「これは n 秒前にブラウザでそのリンクをクリックした本人か?」——答えはプラットフォームによって変わります。

Android:決定論的マッチング。 Detour は Install Referrer API を通じて、一意の click_id を Google Play Store に渡します。アプリの初回起動時に、SDK はまったく同じ click_id を取り戻します。これは 1:1 のマッチングで、正確率は 100% です。Android がリファラルのクリックと初回アプリ起動の間に直接的なデータ経路を提供しているからです。

iOS:確率的マッチング。 Apple は App Store をプライバシーの境界として扱っており、Install Referrer API に相当するものはありません。Detour は代わりに、ブラウザでのクリック時に非識別性のシグナル群のスナップショットを収集し、アプリ起動時に収集した二つ目のスナップショットと突き合わせます。

なぜこれがルーティング層で重要なのか

Detour は +native-intent.tsx を通じてリンクを処理することで、一つのリンクが複数回のナビゲーションを引き起こしたり、初期 URL と衝突したりといったよくある不具合を避けています。

典型的なサードパーティ SDK では、入ってくるリンクは独立したリスナーに落ち、ナビゲーション構造とはそれぞれ別々に動きます。ブラウザのクリックとアプリ内部の状態の間の受け渡しは、自分で処理するしかありません——認証ガードとの衝突、深いネストの問題、重複発火に対処しながら。SDK とルーターは別々の世界に住んでいるのです。

ユーザーが Web から App へ移る瞬間は、ちょうどルーターの層で起きる。だからこそ Expo Router はこのパズルの重要なピースだ。プラットフォームの進化に合わせて Detour を追随させ、この引き継ぎを技術的な副作用ではなく、UX として意図的に設計された一部にしたい。

この先の道

Detour 1.0 は、カスタムドメインや React Native、Flutter、ネイティブ向け SDK といった中核部分を固めた。次はその先だ。ユーザー旅路のより多くの場面をカバーできるようプラットフォームを広げつつ、開発者ファーストと手頃な価格は維持していく。

Detour は現在も活発に開発中で、Expo コミュニティからのフィードバックを歓迎する。問題を見つけた場合や機能の要望があれば、GitHub で issue を立てるか、Discord で声をかけてほしい。

出典: Expo Blog← ホームへ戻る