React Navigationにおける画面遷移時の再レンダリングとuseEffectの発火について

## はじめに
当社のプロダクトは、WEBアプリケーションとスマホアプリの両方を提供しています。
フロントエンドは、ReactでWEB版、React Nativeでスマホ版を開発しています。機能を追加する際は、まずWEB版を実装し、その後スマホ版を実装します。

画面遷移の実装で、WEB版ではうまく動作していたのですが、React Nativeで問題が起きてしまいました。その原因と解決方法を共有します。

## 本題
この記事で伝えたいことはこちらです。

:::note warn
- React Navigationの `go back()`で画面遷移する場合、再レンダリングは行われない
- `useIsFocused`を使うことで画面が最上部に来たことを検知して再レンダリングを行うため、画面遷移したタイミングでuseEffectを発火させることができる
:::
### 要件
- ２つの画面（A、B）を実装する
- 通常は画面Aから画面Bへ遷移する
- 画面Aでは初回描画時にデータをfetchし、表示する
- 画面Bに、画面Aに戻る×ボタンを追加する

### 設計
- 画面B=>Aの遷移はReact Navigationの`go back()`を使う
- 画面B=>Aに遷移したときにも画面AのuseEffectを発火させる

### 問題
React Natvigationの`goBack()`を使って1つ前の画面に遷移したところ、画面Aの初回描画時のfetch処理が実行されませんでした。
```react:ScreenA
const ScreenA = () => {
  useEffect(() => {
  
   // ...fetch処理
  }, [])
};
```
```react:ScrrenB
import { useNavigation } from '@react-navigation/native'

const ScreenB = () => {
    const navigation = useNavigation()
    
    const onClose = (): void => {
        navigation.goBack()
    };
    
    return <button onPress={onClose}>×</button>;
};
```
**なーぜー？？？？？？どこか間違えている？？？？？？**
わからなかったので、先輩に質問しました。

### 原因
`useEffect`が発火しなかったのは、React Navigationの画面遷移の仕様が原因でした。

React Navigationでは画面遷移をStackで管理しています。画面を層としてイメージするのが個人的にしっくりきています。層が上に積み上がっていくようなイメージです。
`goBack()`で前の画面に戻る時は、1番上に積み上がっていた画面が剥がれてその下にいた画面が1番上に出てくるようなイメージになります。

![画面遷移.001.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3537832/fa7ff46b-8f0b-7af3-eea7-2fe618ee4818.jpeg)

そしてこの`goBack()`で画面遷移したときは**再レンダリングが行われません**。ただ画面が1番上に出てきただけで、新たに読み込まれて表示されるわけではないということです。したがって、遷移したときに`useEffect`は発火しません。

### React Routerとの違い
WEB版では画面遷移したタイミングでfetch処理が行われていました。
WEB版の画面遷移の実装は、React Routerの`useNavigate`を使用しています。
引数に遷移する画面のURLを指定して画面遷移します。
このとき、遷移した画面は再レンダリングされるため、画面AのuseEffectは発火したというわけです。
```react
import { Navigate, useNavigate } from 'react-router-dom'

const ScreenB = () => {
    const navigate = useNavigate()
    
    const onClose = (): void => {
      navigate('https://.../screenA')
    }
    
    return <button onClick={onClose}>×</button>;
};
```

### 解決
`useIsFocuse`を使うことで、解決できます🎉
目的は、useEffectの依存配列に画面遷移したことを検知するstateを入れて、遷移したタイミングで発火させることです。

### useIsFocused
https://reactnavigation.org/docs/use-is-focused/

`useIsFocused`はReact Navigationが提供しているhooksです。
**「現在その画面が開かれているか否か？」** を判定し、返り値はbooleanの値です。デフォルトは`false`ですが、その画面が1番上で開かれた場合は`true`に変更されます。
このhooksを使えば、画面が最上部に来たことを検知することができます。
```react
import { useIsFocused } from '@react-navigation/native';

// ...

function Profile() {
  const isFocused = useIsFocused();

  return <Text>{isFocused ? 'focused' : 'unfocused'}</Text>;
}
```
このhooksを使って`isFocused`というstateをuseEffectの依存配列に入れれば、画面が開かれた時は`false`=>`true`に変更されるので変更を検知してuseEffectが発火します。

### 修正
画面Aで`useIsFocused`をimportして、`isFocused`を定義し、画面遷移したときに発火させたいuseEffectの依存配列に入れました。
```react:ScreenA
import { useIsFocused } from '@react-navigation/native';

// 画面B=>Aに遷移したときもfetchできるようにする
const ScreenA = () => {
  const isFocused = useIsFocused()

  useEffect(() => {
    if (!isFocused) {
      return
    }

   // ...fetch処理
  }, [isFocused])
};
```
### 余談
`useIsFocused`を使う以外の方法も教えていただきました。
**画面遷移するときにnavigationのパラメーターに何かstateを渡してそれを検知する方法**です。
React Navigationで遷移する際、navigate関数の第2引数に、遷移先の画面に渡したいパラメーターをオブジェクトに入れて指定することができます。

https://reactnavigation.org/docs/params/
```react
// 遷移先の画面に渡したいパラメーターを指定する
  navigation.navigate('Details', {
    itemId: 86,
    otherParam: 'anything you want here',
  });

// 遷移先の画面で渡されたパラメーターを読み取る
   const { itemId, otherParam } = route.params;
```
このパラーメーターに例えばUUIDなどランダムな文字列を生成して指定します。すると、画面遷移するたびに異なるUUIDを生成することができるので、useEffectの依存配列にUUIDを含めれば、変更を検知してuseEffectを発火させることができます。

しかし、useEffectを発火されるためだけにそこまでするか？というのが正直なところなので、今回は採用しませんでした。

## まとめ
:::note warn
- React Navigationの `go back()`で画面遷移する場合、再レンダリングは行われない
- `useIsFocused`を使うことで画面が最上部に来たことを検知して再レンダリングを行うため、画面遷移したタイミングでuseEffectを発火させることができる
:::

React Navigationでは画面遷移時の動作がReact Routerと異なるため、注意が必要です。
再レンダリングが必要な場合は`useIsFocused`を活用しましょう。

### 参考
https://nopi-tech.hateblo.jp/entry/2020/10/10/140146#%E3%81%BE%E3%81%A8%E3%82%81