React Navigationのlinkingを使用した画面遷移の検討
React Navigationは、ディープリンクを受信した際、URLに応じた画面へ遷移するlinkingという機能を提供しています。
React Navigationのlinkingを使用することで、以下のようにURLと遷移先画面のマッピングを集約して管理できます。またURLを解析してくれるため、クエリパラメータやパスパラメータも取得できます。
const config = {
screens: {
AuthenticatedStackNav: {
screens: {
QuestionDetail: {
// URLのパスが「question/1234」の場合は、QuestionDetail画面に遷移し、パラメータとして「questionId=1234」を渡す
path: 'question/:questionId',
},
},
},
},
}
const linking = {
prefixes: ['https://example.com'], // <- このアプリで受け付けるURLのプレフィックス
config,
};
return <NavigationContainer linking={linking}>{children}</NavigationContainer>
また、想定していないパスを受け取った場合のデフォルトの画面も指定できます。
const config = {
screens: {
AuthenticatedStackNav: {
screens: {
QuestionDetail: {
path: 'question/:questionId',
},
},
},
NotFound: '*', // <- 想定しないパスを受け取った場合はNotFound画面を表示する
},
};
initialRouteNameを指定することで、特定の画面を常にスタックの先頭に追加する機能などもあります。
const config = {
screens: {
AuthenticatedStackNav: {
// ディープリンクからQuestionDetail画面を直接開いても、戻るボタンタップ(navigation.goBack())でHome画面に戻れる
initialRouteName: 'Home',
screens: {
QuestionDetail: {
path: 'question/:questionId',
},
},
},
},
};
このように、linkingはディープリンクを処理する上で非常に便利な機能を提供してくれます。
一方で、認証状態を考慮した画面表示などいくつか検討が必要な点もあったため、以降ではそれらについて記載します。
認証状態による画面表示の制御
React Navigationには、認証フローに関するドキュメントがあります。しかし、linkingと認証フローを組み合わせた場合のドキュメントはなく、以下のissueで議論されています。
受け取ったディープリンクに応じた画面が、未認証で表示してはいけない場合の考慮
認証済の場合は、ディープリンクURLに応じて画面遷移するように、URLと遷移先画面のマッピングを定義します。
未認証の場合は、ディープリンクURLを無視して通常通り起動するように、マッピングは定義しません。
// 認証済みの場合のみ、URLと遷移先画面のマッピングを定義
const config = isLoggedIn ? {
screens: {
AuthenticatedStackNav: {
initialRouteName: 'Home',
screens: {
QuestionDetail: {
path: 'question/:questionId',
},
},
},
},
} : undefined;
未認証時に受け取ったディープリンクの遷移先画面を、認証後に表示する方法
この仕様を実現するための機能がReact Navigationには存在しません。そのため、以下の方法でこの機能を実現します。
- ディープリンクを受け取った時点で、そのディープリンクをグローバルなStateに保持
- コールド、ウォームスタートの場合は、linking.getInitialURL内でStateに設定
- ホットスタートの場合は、linking.subscribe内でStateに設定
- 認証後にStateからディープリンクを取得し、ディープリンクに応じた画面へ遷移
linkingのマッピング定義に従って自動遷移させる方法はないため、独自実装で画面を遷移させる必要がある
認証後にStateからディープリンクを取得し、Linking.openURLを使用してそのディープリンクを開く方法も検討しました。
Linking.openURLを使用してアプリ内からディープリンクを開くことにより、linkingに設定したURLと遷移先画面のマッピングに従って画面遷移が行われると考えたためです。
しかし、この方法ではiOSの場合に画面遷移が行われず、ブラウザでディープリンクが開かれてしまいました。
ディープリンクの遷移先画面で戻るボタンをタップした場合の動作
linkingのデフォルト動作の確認
linkingを使用して画面遷移する場合、navigation.navigateを使用した場合と同様の動作になります。
navigation.navigateは、Moving between screens - Summaryに記載されている通り、以下の特徴を持っています。
- 対象の画面がナビゲーションスタック内に存在している場合は、その画面まで戻る
- 対象の画面がナビゲーションスタック内に存在していなければ、画面をスタックに追加する
ナビゲーションスタックの状態によっては、ディープリンクをタップする前に表示していた画面にnavigation.goBackなどで戻れない事が予想できます。
以下に例を記載します。
ユーザは、アプリでScreenCを表示しています。
- StackNavigator1
- ScreenA
- ScreenB
- ScreenC <- この画面が表示されている
ユーザがScreenBに遷移するディープリンクをタップすると、スタック内に既に存在するScreenBまで戻ります。
- StackNavigator1
- ScreenA
- ScreenB <- この画面が表示されている
この場合、ScreenBからnavigation.goBackを実行した場合、ScreenAに戻ってしまいます。
もう1つ例を記載します。
ユーザは、アプリでStackNavigator2内のScreenFを表示しています。
- StackNavigator1
- ScreenA
- ScreenB
- ScreenC
- StackNavigator2
- ScreenD
- ScreenE
- ScreenF <- この画面が表示されている
ユーザがScreenBに遷移するディープリンクをタップすると、まずStackNavigator1まで戻ります。その後、StackNavigator1のスタック内に存在するScreenBまで戻ります。
- StackNavigator1
- ScreenA
- ScreenB <- この画面が表示されている
この場合、ScreenBからnavigation.goBackを実行した場合、ScreenAに戻ってしまいます。
これらの挙動を回避する方法として、以下を検討しました。
linking.getActionFromStateを使用して、Navigation actionを設定する- 遷移先画面の
Screen.getIdで、ディープリンク受信時は常にユニークなIDを返却する
linking.getActionFromStateを使用して、Navigation actionを設定する
linking.getActionFromStateは、Navigation actionを設定するための関数です。引数で、Navigation stateを受け取るため、画面に応じてNavigation actionを設定できます。
import {NavigationContainer, getActionFromState as getOriginalActionFromState} from '@react-navigation/native';
const getActionFromState: typeof getOriginalActionFromState = useCallback((state, options) => {
const action = getOriginalActionFromState(state, options);
// 画面に応じてNavigation actionを設定
if (ScreenAの場合) {
return {...action, type: 'PUSH'};
} else if (ScreenBの場合) {
return {...action, type: 'NAVIGATE'};
} else {
return {...action, type: 'RESET'};
}
}, []);
const linking = {
...
getActionFromState,
};
return <NavigationContainer linking={linking}>{children}</NavigationContainer>;
しかし、linking.getActionFromStateは、React Navigationのドキュメントでは公開されておらず、積極的に使用するには不安が残ります。また、Navigation stateの構造は複雑であるため、遷移先画面に応じた条件分岐の作成は難しくなると考えられます。
linkingには、Advanced casesに記載されているように、linking.getStateFromPathという機能もあります。
この機能を使用することにより、URLからNavigation stateの状態を自由に作成できます。
そのため、linking.getActionFromStateとlinking.getStateFromPathを組み合わせることで、自由度の高い画面遷移が実現可能です。
しかし、これらの実装は非常に複雑な処理になることが予想されます。URLの解析やNavigation stateの生成を自身で実装することは、linkingを使うことで得られる多くのメリットを失います。
遷移先画面のScreen.getIdで、ディープリンク受信時は常にユニークなIDを返却する
navigation.navigateは、Screen.nameとScreen.getIdの返却値によって、画面遷移の挙動が変わります。
同一のnameとgetIdの返却値を持つScreenがナビゲーションスタックに存在している場合、その画面まで戻ります。そうではない場合、画面をナビゲーションスタックに追加します。
この特徴を利用して、ディープリンク受信時はgetIdからユニークなIDを返却することで、navigation.pushと同様の挙動を実現できます。
// クエリパラメータとして、linking=trueを受け取った場合は、ユニークなIDを返却する
export const RootStackNavigator: React.FC = () => {
return (
<nav.Navigator>
<nav.Screen
name="StackNavigator1"
component={StackNavigator1}
getId={({params}) => {
if (params?.screen === 'ScreenB') {
return params.params?.linking ? String(Date.now()) : undefined;
}
return undefined;
}}
/>
</nav.Navigator>
);
};
const StackNavigator1: React.FC = () => {
return (
<nav.Navigator>
<nav.Screen name="ScreenA" component={ScreenA} />
<nav.Screen
name="ScreenB"
component={ScreenB}
getId={({params}) => (params.linking ? String(Date.now()) : undefined)}
/>
<nav.Screen name="ScreenC" component={ScreenC} />
</nav.Navigator>
);
};
しかし、この方法の場合は、Navigation actionとしてNAVIGATEとPUSH以外のものを使用できません。
Screen.getIdを使用してNavigation actionをPUSHに変更する方法は、以下のissueで議論されています。