Help us understand the problem. What is going on with this article?

React Navigation v5 傾向と対策

はじめに

この記事は React Native Advent Calendar 2019 10 日目の記事です。
昨日は @tkow さんの記事で、 React Native 0.60 で導入されたネイティブモジュールの autolinking に関する解説でした。

この記事は、 React Navigation の次バージョンである v5 について、機能や変更点などを予習しておこうというような内容です。

主な情報源は以下になります。

GitHub や公式ドキュメントは正式リリース後にリンク切れになる可能性があります。

v4 までのあらすじ

React NavigationReact Native 公式ドキュメントのナビゲーションのページ にも一番目に紹介されている、 React Native でナビゲーションを行う際の最もスタンダードなライブラリです。
ネイティブモジュールへの依存が比較的少なく、 Expo でも簡単に動かすことができるのが特徴です。

ここで、 React Navigation の今までの大まかな変遷を振り返ってみます。

beta (2017/02)

  • 詳しい状況は知らないのですが、正式リリースまでの期間が長かったため beta だったにも関わらず本番投入されることが多い印象でした

v1 (2018/02)

  • 今まで推奨されていた Redux 統合が deprecated になり、 react-navigation-redux-helpers として分離
  • 人気のあった react-native-router-flux が v4 から react-navigation に依存するようになり、ナビゲーションライブラリの事実上のスタンダードとしての地位を確立

v2 (2018/05)

  • XNavigator(...) が deprecated になり、代わりに createXNavigator(...) を使うようになる
  • TabBarcreateBottomTabBar, createMaterialTopTabBar, createMaterialBottomTabBar に分割

v3 (2018/11)

  • トップレベルのナビゲーションを createAppContainer で囲うように変更
  • 内部的にナビゲーションの実装を react-navigation-stack, react-navigation-tabs, react-navigation-drawer パッケージに分離
  • 途中で react-navigation パッケージに TypeScript の型情報追加

v4 (2019/09)

  • v3 で分離されたパッケージが react-navigation を通して提供されなくなり、個別にインストールするように変更

メジャーバージョンが変わるごとに互換性のない変更があったものの、基本的にバージョンアップに必要な作業は関数名やパッケージ名の変更程度でした。

それでは、 v5 の変更点を見ていきましょう。

ナビゲーションを JSX で定義するようになり、動的に変化させることが可能になる

v4 以前

v4 までは、以下のように createXNavigator 関数ににオブジェクトを渡してナビゲーションを定義していました。 v1 では XNavigator という名前の関数でしたが、基本は同じでした。

const StackNav = createStackNavigator({
  Home: { screen: HomeScreen },
  Profile: { screen: ProfileScreen },
  Settings: { screen: SettingsScreen }
});

この createXNavigator 関数は render 内で呼んではならず、予め定義しておく必要がありました。そのため例えばニュースアプリ等で動的にカテゴリ一覧を取得してカテゴリ毎にタブを表示したい等といった場合は、ナビゲーションを表示する前に予めカテゴリ一覧を取ってきて、クラスコンポーネントのコンストラクタでナビゲーションを定義する等工夫する必要がありました。

v5

v5 では、以下のように render 内に JSX でナビゲーションを定義するようになります。 Web の React でよく使われる react-router や前述の react-native-router-flux に近い書き味です。

const Stack = createStackNavigator();

const StackNav = () => (
  <Stack.Navigator>
    <Stack.Screen name="Home" component={HomeScreen} />
    <Stack.Screen name="Profile" component={ProfileScreen} />
    <Stack.Screen name="Settings" component={SettingsScreen} />
  </Stack.Navigator>
);

createXNavigatorNavigatorScreen というコンポーネントを返すので、それを使って定義します。 React.createContextProviderConsumer を返す関係に似ています。記述量は v4 までに比べて若干増えてしまっています。

なんとこの書き方では通常のコンポーネントと同様に条件分岐やループが可能です。

const TabNav = () => (
  const tabs = useState(['Tab1', 'Tab2']);

  return (
    <Tab.Navigator tabBarOptions={{ scrollEnabled: true }}>
      {tabs.map(tab => (
        <Tab.Screen key={tab} name={tab}>
          {() => (
            <View><Text>{tab}</Text></View>
          )}
        </Tab.Screen>
      ))}
    </Tab.Navigator>
  );
);

この例では、 tabs を state として定義していますので、要素が増減するとナビゲーションもそれに追従します。

また、 この書き方のように Screen の中身を component ではなく子コンポーネントに Render Callback として渡すことも可能です。こうすることで動的に props を渡すことが可能になりますが、 component に渡した場合は不要な render が呼ばれないようにライブラリ側で最適化してくれるので、基本的には component に渡すのが推奨なようです。 (READMEより

オプションを render 内でセットできるようになる

v4 以前

v4 までは、 navigationOptions も render 外で予め定義しておく必要がありました。コンポーネント側に設定する方法とナビゲーション側に設定する方法がありましたが、どちらにしてもコンポーネント内とデータをやりとりするには以下のように params を介してハンドラを渡す 必要がありました。

const EditScreen = props => {
  const { navigation } = props;

  const onSubmit = useCallback(...);

  useEffect(() => {
    // setParams する条件に気をつけないと無限に render されるので注意!!
    if (onSubmit !== navigation.getParam('onSubmit')) {
      navigation.setParams({ onSubmit });
    }
  }, [navigation, onSubmit]);

  // ...
};

// コンポーネント側で設定する場合
EditScreen.navigationOptions = ({ navigation }) => ({
  title: 'Edit',
  headerRight: () => (
    <Button onPress={navigation.getParam('onSubmit')}>
      <Text>Submit</Text>
    </Button>
  )
});


const StackNav = createStackNavigator({
  Edit: {
    screen: EditScreen,
    // ナビゲーション側で設定する場合
    navigationOptions: ({ navigation }) => ({
      title: 'Edit',
      headerRight: () => (
        <Button onPress={navigation.getParam('onSubmit')}>
          <Text>Submit</Text>
        </Button>
      ),
    }),
  },
  // ...
});

v5

v5 では render 内で navigation.setOptions を呼び出してオプションをセットする事が可能になります。

const EditScreen = props => {
  const { navigation } = props;

  const onSubmit = useCallback(...);

  // コンポーネント内で設定する場合
  navigation.setOptions({
    title: 'Edit',
    headerRight: () => (
      <Button onPress={onSubmit}>
        <Text>Submit</Text>
      </Button>
    )
  });

  // ...
};

const Stack = createStackNavigator();

const StackNav = () => (
  <Stack.Navigator>
    {/* ナビゲーション側で設定する場合 */}
    <Stack.Screen name="Edit" component={Edit} options={{ ... }} />
  </Stack.Navigator>
);

がっつりと副作用を起こしているように見えるのですがドキュメントを見る限り useEffect の中ではなく render 直下に書くようです。パフォーマンス面までは検証が間に合いませんでした。

また、引き続きナビゲーション側でも options として渡すことができますが、この場合は従来どおり getParam を介する必要があるので、動的なオプションの場合コンポーネント内で setOptions したほうが良さそうです。

TypeScript 対応強化

v4 以前

型の当て方は色々な方法がありますが、 v4 で関数コンポーネントとして定義する場合は以下のように書くとシンプルです。

type Params = {
  id: string
};

type ScreenProps = {};

const DetailScreen: NavigationStackScreenComponent<
  Params,
  ScreenProps,
> = props => {
  const { navigation } = props;

  // 型推論が効く
  const id = navigation.getParam('id');

  const onPress = useCallback(() => {
    // routeName は string, params は any
    navigation.navigate('Comments', { id });
  }, [navigation]);

  // ...
};

そのコンポーネントがマウントされるスクリーンの params や screenProps を定義することはできましたが、他のスクリーンへの navigate のときに routeName や params に正しい型情報を与えるのは困難でした。

v5

ナビゲーション全体を表す ParamList という型を先に定義しておき、それを使う形に変更になります。

type StackParamList = {
  Home: undefined;
  Detail: { id: string };
  Comments: { id: string };
};

type DetailScreenNavigationProp = StackNavigationProp<StackParamList, 'Detail'>;
type DetailScreenRouteProp = RouteProp<StackParamList, 'Detail'>;

type Props = {
  navigation: DetailScreenNavigationProp;
  route: DetailScreenRouteProp;
};

const DetailScreen: React.FC<Props> = props => {
  const { navigation, route } = props;

  // 型推論が効く
  const { id } = route.params;

  const onPress = useCallback(() => {
    // routeName の型推論が効き、 params に id が必須になる
    navigation.navigate('Comments', { id });
  }, [navigation]);

  // ...
}

navigation.navigate に型推論が効くのは嬉しいです。ただし、ネストしたナビゲーションの場合 CompositeNavigationProp を使って NavigationProp をマージしていくのですが、そちらで試したところ現在は navigation.navigate の型推論が効きませんでした。
書き方としては画面ごとに NavigationProp と RouteProp を定義していくのは辛く今までのほうがシンプルに感じるので、今後の改善に期待です。

その他

  • createNativeStackNavigator という、内部でネイティブのナビゲーションを使うナビゲーションが新しく追加されます
  • 現在は react-navigation-hooks を通して提供されている、 useNavigation などの Hooks を標準で提供するようになります
  • ひっそりと Web Support のページが追加されており、 今後 react-native-web に対応することで Expo の Web 対応を強化していく方向性になると予想されます

まとめ

React Navigation v5 は react-navigation/navigation-ex リポジトリで開発中です。まだ alpha 段階で正式リリースまでは見たところ早くても数ヶ月はかかるかなという感じですし、 v4 との互換機能 も用意されているようなのでそこまでトラブルはさそうですが、このように変わるということだけでも知っておいて損はないかなと思いました。

今回手元で色々と試した内容を GitHub にプッシュしておりますので、興味があれば参考にしてみてください。
https://github.com/shinnoki/react-navigation-v5-example

wasd-inc
『意思疎通をもっと便利に』をミッションに掲げるスタートアップです。 呼び出し業務を革新するクラウドサービス『デジちゃいむ』を開発しています。
https://digichime.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした