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 はこれに直接フックします:
import { createDetourNativeIntentHandler } from "@swmansion/react-native-detour/expo-router";
export const redirectSystemPath = createDetourNativeIntentHandler({
fallbackPath: "/",
hosts: [/\.godetour\.link$/i],
});
これはプリルーティングのミドルウェアとして機能します。アプリが起動すると、Detour はこれが遅延ディープリンクかどうかをチェックし、元の URL を解決して、どの画面もマウントされる前にルーターに渡します。
認証ゲートの処理
一般的な落とし穴が認証ゲートです。ユーザーが招待リンクをタップし、アプリをインストールして開くと、Detour は元の URL を解決します。しかしユーザーはまだログインしていないため、あなたの認証ガードは /login にリダイレクトします。その結果、解決されたリンクは飲み込まれてしまいます。
DetourProvider は、アプリがユーザーの準備ができたことをシグナルするまでリンクの意図をメモリに保持することでこれを解決します:
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();
const router = useRouter();
useEffect(() => {
if (isLinkProcessed && link && isSignedIn) {
clearLink();
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 で連絡してください!