production.log

株式会社リブセンスでエンジニアをやっている星直史のブログです。

Snapmartアプリで使用しているReact Navigationをv4からv5にアップグレードした際の対応

概要

2020年2月にReact Navigationのv5がリリースされました。
React Navigation 5.0 - A new way to navigate | React Navigation

Snapmartアプリでは、React Navigation v4を使用しています。
React Navigation v4では、Stack NavigatorにJavaScriptを使用していましたが、ネイティブのような感覚とパフォーマンスが低下する場合がありました。
React Navigation v5では、Native navigation primitivesを使用するStack Navigatorに変更することで改善がされました。

Stack Navigatorは多くの画面遷移で使用するため、このタイミングでアップグレードすることにしました。

今回は、Snapmartアプリで使用しているReact Navigationをv4からv5にアップグレードした際の対応についてまとめます。

手順

React Navigationはアップグレードガイドが非常に丁寧に書かれています。

Upgrading from 4.x | React Navigation

このリストの中で、実際に使用している処理については対応をしていく必要があります。 対応したことは以下の通りです。

  1. パッケージ名の変更
  2. createAppContainerからNavigationContainerに変更
  3. コンポーネント化されたN Navigatorに変更
  4. navigation propから分割されたroute propを使用
  5. navigationOptions を各Screenで設定するように修正
  6. Navigation eventsの変更
  7. ディープリンクの記述修正
  8. CommonActionsの変更

1. パッケージ名の変更

各パッケージ名が変更されたので、記載されている内容にしたがって変更をしていきます。

Upgrading from 4.x | React Navigation

若干ハマった点としては、パッケージ名を変更して起動をするとこのようなエラーが発生しました。

I'm getting "SyntaxError in @react-navigation/xxx/xxx.tsx" or "SyntaxError: /xxx/@react-navigation/xxx/xxx.tsx: Unexpected token"

というエラーが表示されたので、以下のコマンドで対応します。

$ rm -rf node_modules
$ rm yarn.lock
$ yarn

2. createAppContainerからNavigationContainerに変更

こちらは単純にcreateAppContainerからNavigationContainerに変更します。
Upgrading from 4.x | React Navigation

3. コンポーネント化されたN Navigatorに変更

React Navigation v5の大きな変更として、コンポーネント化があります。

React Navigation v4では以下のような記述でした。

const RootStack = createStackNavigator(
  {
    Home: {
      screen: HomeScreen,
      navigationOptions: { title: 'My app' },
    },
    Profile: {
      screen: ProfileScreen,
      params: { user: 'me' },
    },
  },
  {
    initialRouteName: 'Home',
    defaultNavigationOptions: {
      gestureEnabled: false,
    },
  }
);

React Navigtaion v5からは以下のような書き方になります。

const Stack = createStackNavigator();

function RootStack() {
  return (
    <Stack.Navigator
      initialRouteName="Home"
      screenOptions={{ gestureEnabled: false }}
    >
      <Stack.Screen
        name="Home"
        component={HomeScreen}
        options={{ title: 'My app' }}
      />
      <Stack.Screen
        name="Profile"
        component={ProfileScreen}
        initialParams={{ user: 'me' }}
      />
    </Stack.Navigator>
  );
}

変更を自動化できなさそうなので、粛々と修正していきました。

4. navigation propから分割されたroute propを使用

React Navigation v5からは、navigation propからroute propが切り出されました。 影響を受けた点としては、画面遷移時に渡すparameterがroute propから取得しなければならなくなったことです。

- this.props.navigation.state.params.id
+ this.props.route.params.id

こちらの対応は、sedで一括置換で対応しました。

find src/ -type f -print0 | xargs -0 sed -i -e "s/navigation\.state/route/g"

5. navigationOptions を各Screenで設定するように修正

navigationOptionsもv4では各画面で設定していましたが、v5からはScreenコンポーネントで設定します。
Upgrading from 4.x | React Navigation

こんな感じです。

v4

class ProfileScreen extends React.Component {
  static navigationOptions = {
    headerShown: false,
  };

  render() {
    // ...
  }
}

v5

<Stack.Screen
  name="Profile"
  component={ProfileScreen}
  options={{ headerShown: false }}
/>

これも自動で変換はできなさそうなので、手で粛々と直します。

6. Navigation eventsの変更

Upgrading from 4.x | React Navigation

v4では、Navigation evenetが4種類ありました。

  • willFocus
  • didFocus
  • willBlur
  • didBlur

v5ではシンプルにfocusblurだけになります。これは、v4におけるwillFocuswillBlurです。

また、書き方も変わります。 Class Componentにおいてはこのようになります。

+ componentDidMount() { 
+   const unsubscribe = this.props.navigation.addListener("focus", e => {
+     // 処理
+   });
+ }
render() {
    return (
      <View style={styles.container}>
-        <NavigationEvents onWillFocus={payload => { console.log("onWillFocus") }} />

7. ディープリンクの記述修正

Deep linking | React Navigation
ディープリンキングは、pathのハンドリングが変わります。

import { Linking } from 'expo';
import { NavigationContainer, useLinking } from "@react-navigation/native"; 

const prefix = Linking.makeUrl('/');

function App() {
  const ref = React.useRef();

  const { getInitialState } = useLinking(ref, {
    prefixes: [prefix],
    config: {
      HomeStack: {
        path: 'stack',
        screens: {
          Home: 'home',
          Profile: 'user',
        },
      },
      Settings: 'settings',
    }
  });

  const [isReady, setIsReady] = React.useState(false);
  const [initialState, setInitialState] = React.useState();

  React.useEffect(() => {
    getInitialState()
      .catch(() => {})
      .then(state => {
        if (state !== undefined) {
          setInitialState(state);
        }

        setIsReady(true);
      });
  }, [getInitialState]);

  if (!isReady) {
    return null;
  }

  return (
    <NavigationContainer initialState={initialState} ref={ref}>
      {/* content */}
    </NavigationContainer>
  );
}

refを定義し、NavigationContaineruseLinkingに渡します。 また、pathの定義は、useLinkingconfigで画面を紐づけを行います。

上記の例は、HomeStackとSettingsの2つのタブがあり、 HomeStackタブには、HomeとProfileという2つのスクリーンが存在するイメージです。

この定義だとmyapp://homeでHomeスクリーンにリンクすることができます。

8. CommonActionsの変更

CommonActions reference | React Navigation
最後に、CommonActionにも変更がありました。
resetを使っていた箇所があったので、修正をします。

+ import { CommonActions } from '@react-navigation/native';
...
-    let resetAction = StackActions.reset({
-      index: 0,
-      actions: [NavigationActions.navigate({ routeName: "Account" })],
-    });
-    this.props.navigation.dispatch(resetAction);
+    this.props.navigation.dispatch(
+      CommonActions.reset({
+        index: 0,
+        routes: [{ name: "Account" }],
+      })
+    )

まとめ

対応は以上です。

  1. パッケージ名の変更
  2. createAppContainerからNavigationContainerに変更
  3. コンポーネント化されたN Navigatorに変更
  4. navigation propから分割されたroute propを使用
  5. navigationOptions を各Screenで設定するように修正
  6. Navigation eventsの変更
  7. ディープリンクの記述修正
  8. CommonActionsの変更

Snapmartは78画面で構成されるアプリですが、手作業の部分が多く、骨が折れました...。
特に、コンポーネント化されたN Navigatorに変更とnavigationOptions を各Screenで設定するように修正する作業は、全ての画面を開いて修正しなけばならなかったのが大変でした。