【React Navigation 6】画面遷移時に既存のパラメータを保持したいときは`merge: true` オプションを指定しよう

## はじめに
React Nativeの開発で、画面遷移にはReact Navigationのnavigate関数を使用しています。第２引数としてパラメータを遷移先の画面に渡すことができます。

https://reactnavigation.org/docs/navigation-prop#navigate

この仕組みを使って実装していたところ、なんか思ってたんと違う😕挙動を確認できました。調べてみたところ、デフォルトでその動きになるように仕様が変わっていたようなのでまとめます。

先に結論を書きます。

:::note info
- React Navigation 6系以降では、画面遷移時にパラメータがデフォルトで上書きされるようになった
- 既存のパラメータを維持したい場合は、navigate関数を使用する際に`merge: true`オプションを指定する
```typescript
navigation.navigate({
  name: 'ScreenName',
  params: { newParam: 'value' },
  merge: true
});
```
:::


## 発生した問題
ScreenAとScreenBの2画面があります。
ScreenAからScreenBに移動し、ScreenBでuserIdsを取得します。その後、ScreenBからScreenAに戻ります。このとき、取得したuserIdsをパラメータとしてScrrenAに渡します。
コードに表すと以下のようになります。
```typescript:ScreenB
export const ScreenB: FC = () => {
// ...userIdsを取得

// ScreenAに遷移するとき、userIdsをパラメータで渡す
 navigation.navigate('ScreenA', { userIds })
  
// ...
}
```
ScreenAでパラメータにアクセスしてみると、userIdsが正しく渡ってきていることを確認できました🎉
```typescript:ScreenA
import { useRoute } from '@react-navigation/native'
import { Text } from 'react-native-svg'

export const ScreenA: FC = () => {
 const route = useRoute<ScreenARouteProp>()
// ...省略
 const { name, userIds } = route.params

 return (
    <Text>{name}</Text>
 )
}
```
しかし、思わぬところで事件が起きます。
初めてScreenAが開かれるときは、ScreenCから遷移してきます。このときScreenCからパラメーターとしてnameを受け取っています。
このnameを画面に描画しているのですが、ScreenBからScreenAに遷移したとき、表示できていたはずのnameが表示されなくなってしまいました😱

## 原因
この原因は、React Navigationの仕様によるものです。

公式ドキュメントによると、6系から**画面遷移時にパラメータはマージではなく上書きされるようになった**らしいです。

https://reactnavigation.org/docs/upgrading-from-5.x/#params-are-now-overwritten-on-navigation-instead-of-merging

以下、公式ドキュメントの例を参考に仕様をまとめます。

5系以前の仕様では、新しいパラメータが既存のパラメータにマージされていました。
マージとは、**新しいパラメータと既存のパラメータが結合される**ことを意味します。このとき、**キーが重複する場合は新しい値で上書きされ、新しいパラメータで指定されていないプロパティは、既存の値が保持されます。**

例えば、以下のパラメータを持つ既存の「Post」画面があるとします。
```javascript:Post
{
  postTitle: 'An amazing post',
  postBody: 'Amazing content for amazing post'
}
```
そして、`navigation.navigate('Post', { postTitle: 'An okay post' })`で遷移すると、以下のパラメータを持つことになります。
postTitleは値が更新されていますが、postBodyの値はそのまま引き継がれています。
```javascript:Post
{
  postTitle: 'An okay post',
  postBody: 'Amazing content for amazing post'
}
```
この仕様は、ユーザーからのバグ報告が多発してしまったみたいです。
パラメーターを渡していないのに、元のパラメータが保持されてしまうのは気持ち悪いということでしょうか…😕

6系からはこの仕様が変更になりました。
**パラメータがマージされず、新しいパラメータが既存のすべてのパラメータを上書きする**ようになりました。
よって、`navigation.navigate('Post', { postTitle: 'An okay post' })`でナビゲートすると、元のpostBodyプロパティは消えて以下のパラメータを持つことになります。
```javascript
{
  postTitle: 'An okay post'
}
```
問題の実装を振り返ってみましょう。
screenBからScreenAに遷移するとき、`navigation.navigate('ScreenA', { userIds })`として、新しく`{ userIds }`を渡しています。
これにより、元々ScreenAが保持していたパラメータが上書きされ、nameプロパティが消えてしまったので描画できなくなってしまったというわけです。


## 解決
6系からはデフォルトでパラメータがマージされず、新しいパラメータが既存のすべてのパラメータを上書きしてしまいますが、必要に応じてこの設定を変更することができます！！
navigateに`merge: true`を含むオブジェクトを渡すことでパラメータをマージできます🎉
以下のように、`merge: true` オプションを指定しましょう。
```javascript
navigation.navigate({
  name: 'Post',
  params: { postTitle: 'An okay post' },
  merge: true,
});
```

では、これを踏まえて問題のコードを修正してみましょう。
```typescript:ScreenB
 navigation.navigate({
    name: 'ScreenA',
    params: { userIds },
    merge: true
  });
```
これで、screenAが保持していたnameプロパティは上書きされずそのまま保持され、新しく渡ってきたuserIdsはパラメータに追加されます。

## 余談
useRouteの型引数ScreenAParamsは以下のように指定しています。
ScreenBから遷移するときは、nameプロパティは渡されないのでundefindとなります。
`merge: true`を指定しない場合、nameプロパティはundefindとして上書きされてしまいました。
```typescript
import { RouteProp } from '@react-navigation/native';

type ScreenAParams = 
 | {
     // ScreenCからの遷移
      userIds?: never
      name: string
    }
  | {
     // ScreenBからの遷移
      userIds: string[]
      name?: never
    };

type ScreenARouteProp = RouteProp<{ ScreenA: ScreenAParams }, 'ScreenA'>;
```
## 終わりに
ライブラリのデフォルトの挙動に振り回されてしまいました😢
画面遷移する時はパラメーターが上書きされてしまう仕様だと覚えておきます💡