React Navigationで、ネストされたStackの遷移元によって戻る先を出し分けたい場合があったので、その対処法を記事にしました。
環境
- Expo
- TypeScript
- ReactNavigation
セットアップ
型安全なプロジェクトを作成します
- expoプロジェクトを作成
$ expo init navigation-test
- テンプレートで、blank (Typescript)を選択
- 必要なパッケージをインストール
$ yarn add @react-navigation/native @react-navigation/native-stack $ expo install react-native-screens react-native-safe-area-context
- 公式を参考に以下を実装
// App.tsx import { NavigationContainer, useNavigation } from '@react-navigation/native'; import { createNativeStackNavigator, NativeStackNavigationProp } from '@react-navigation/native-stack'; import React from 'react'; import { Button, Text, View } from 'react-native'; import { RootStackParamList } from './types'; const RootStack = createNativeStackNavigator<RootStackParamList>(); export default function App() { return ( <NavigationContainer> <RootStack.Navigator initialRouteName="A"> <RootStack.Screen name="A" component={AScreen}/> <RootStack.Screen name="B" component={BScreen}/> <RootStack.Screen name="C" component={CScreen}/> </RootStack.Navigator> </NavigationContainer> ); } const AScreen: React.FC = () => { const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>(); return ( <View> <Text>A Screen</Text> <Text>{JSON.stringify(rootNavigation.getState().routes)}</Text> <Button title="Go To B Screen" onPress={() => rootNavigation.navigate('B')} /> </View> ) } const BScreen: React.FC = () => { const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>(); return ( <View> <Text>B Screen</Text> <Text>{JSON.stringify(rootNavigation.getState().routes)}</Text> <Button title="Go To B Screen" onPress={() => rootNavigation.navigate('B')} /> </View> ) } const CScreen: React.FC = () => { const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>(); return ( <View> <Text>C Screen</Text> <Text>{JSON.stringify(rootNavigation.getState().routes)}</Text> <Button title="Go To B Screen" onPress={() => rootNavigation.navigate('B')} /> </View> ) } // types.ts export type RootStackParamList = { A: undefined; B: undefined; C: undefined; }
簡単なコードの解説
const RootStack = createNativeStackNavigator<RootStackParamList>();
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
- Stackの型を渡して、rootNavigationを定義しています。rootNavigationをconsole.logすると、画面遷移に関係する様々なメソッドが取れます↓
Object { "addListener": [Function addListener], "canGoBack": [Function canGoBack], "dispatch": [Function dispatch], "getId": [Function getId], "getParent": [Function getParent], "getState": [Function anonymous], "goBack": [Function anonymous], "isFocused": [Function isFocused], "navigate": [Function anonymous], "pop": [Function anonymous], "popToTop": [Function anonymous], "push": [Function anonymous], "removeListener": [Function removeListener], "replace": [Function anonymous], "reset": [Function anonymous], "setOptions": [Function setOptions], "setParams": [Function anonymous], }
- この中のgetStateメソッドのroutesで、過去に積まれたscreenの履歴を見れるので、Screen A、B、Cを作成して、そこで積まれているroutesを画面に表示しています
- 各画面のナビゲーション先は、一旦全てBにしています
基本的なnavigationの挙動確認
AからBに画面遷移する
- AからB遷移時に、routesに新しいscreeen Bが積まれる
- 戻るをタップすると、一つ前の画面Aに戻り、screeen Bが削除される
- 内部的には、navigation.goBack()が発火していて、routesに積まれている一つ前のscreen Aにnavigate(’A’)している
BからBに遷移する
- routesにすでにBが積まれていて、現在Bにいるため動かない
Bから新しいBに遷移する
- ただし、Bへの遷移をnavigateではなく、pushメソッドを使うことで新たなscreenをroutesに登録、画面遷移できます
// B Screen <View> <Text>B Screen</Text> <Text>{JSON.stringify(rootNavigation.getState().routes)}</Text> <Button title="Go To New B Screen" // onPress={() => rootNavigation.navigate('B')} onPress={() => rootNavigation.push('B')} /> </View>
積まれていないrouteに戻る
- ここでBから、routesに積まれていないCに戻ったらどうなるでしょうか?
- BScreenに以下のコードを追記して、戻るボタンをカスタマイズします
// B Screen useFocusEffect( useCallback(() => { rootNavigation.setOptions({ headerLeft: () => ( <Button title="Go Back C Screen" onPress={() => rootNavigation.navigate('C')} /> ), }); }, []), );
- routeにCが存在しないので、新たなScreenCがroutesに積まれて画面遷移するのがわかります
遷移元により戻る先を変動させる
- 長くなりましたがここから本題です。これらのStackはネストすることができるのですが、異なるStack間の画面遷移時に意図した通りのroutesが積まれず、navigationに用意されているメソッドでは理想とする画面遷移を実現できないケースが出てきました。
例えば以下の構成で、HelpからHelpDetailに画面遷移した後はHelpに戻りたい一方、UserからHelpDetailに遷移した場合はUserに戻りたい場合、UserとHelpDetailは異なるStackに属するのでgoBackができないなど問題が発生しました。
RootStack
- Home
- TabNavigator
- HelpStack
- Help
- HelpDetail
- XXXStack
- HelpStack
- TabNavigator
- User
- XXX
- Home
対処法としては、HelpDetailにパラメータを渡して、そのパラメータによって戻り先を変更することができます。
Aからの遷移だった場合のみCに遷移させる
- これまでの例で見ていきます。
- BにAからの遷移であるかを示すパラメータisFromAを渡します。
// types.ts export type RootStackParamList = { A: undefined; // B: undefined; B: { isFromA: boolean }; C: undefined; } // AScreen <View> <Text>A Screen</Text> <Text>{JSON.stringify(rootNavigation.getState().routes)}</Text> <Button title="Go To B Screen" // onPress={() => rootNavigation.navigate('B')} onPress={() => rootNavigation.navigate('B', { isFromA: true })} /> </View>
- useRouteを使って、Bに渡されるパラメータを取得できます
// B Screen const { params } = useRoute<RouteProp<RootStackParamList, 'B'>>()
- isFromAがtrueの時のみ、戻り先をCに固定します
useFocusEffect( useCallback(() => { rootNavigation.setOptions({ // headerLeft: () => ( // <Button // title="Go Back C Screen" // onPress={() => rootNavigation.navigate('C')} // /> // ), headerLeft: () => ( params.isFromA ? ( <Button title="Go Back C Screen" onPress={() => rootNavigation.navigate('C')} /> ) : ( <Button title="Go Back" onPress={() => rootNavigation.goBack()} /> ) ) }); }, []), );
挙動確認
routesにCが存在しないため、新たなscreenCが積まれて画面遷移してはいますが、遷移元がAのとき正しくCに戻れています
最後にCからBに遷移したときの挙動確認です。初期表示するScreenを設定できるinitailRoutesをCに変更して検証します。
// App.tsx export default function App() { return ( <NavigationContainer> // <RootStack.Navigator initialRouteName="A"> <RootStack.Navigator initialRouteName="C"> <RootStack.Screen name="A" component={AScreen}/> <RootStack.Screen name="B" component={BScreen}/> <RootStack.Screen name="C" component={CScreen}/> </RootStack.Navigator> </NavigationContainer> ); } // C Screen <View> <Text>C Screen</Text> <Text>{JSON.stringify(rootNavigation.getState().routes)}</Text> <Button title="Go To B Screen" // onPress={() => rootNavigation.navigate('B')} onPress={() => rootNavigation.navigate('B', { isFromA: false })} /> </View> )
- 遷移元がCのときは正しくCに戻れることが確認できました!