はじめに
当社のプロダクトは、WEBアプリケーションとスマホアプリの両方を提供しています。
フロントエンドは、ReactでWEB版、React Nativeでスマホ版を開発しています。機能を追加する際は、まずWEB版を実装し、その後スマホ版を実装します。
画面遷移の実装で、WEB版ではうまく動作していたのですが、React Nativeで問題が起きてしまいました。その原因と解決方法を共有します。
本題
この記事で伝えたいことはこちらです。
- React Navigationの
go back()
で画面遷移する場合、再レンダリングは行われない -
useIsFocused
を使うことで画面が最上部に来たことを検知して再レンダリングを行うため、画面遷移したタイミングでuseEffectを発火させることができる
要件
- 2つの画面(A、B)を実装する
- 通常は画面Aから画面Bへ遷移する
- 画面Aでは初回描画時にデータをfetchし、表示する
- 画面Bに、画面Aに戻る×ボタンを追加する
設計
- 画面B=>Aの遷移はReact Navigationの
go back()
を使う - 画面B=>Aに遷移したときにも画面AのuseEffectを発火させる
問題
React NatvigationのgoBack()
を使って1つ前の画面に遷移したところ、画面Aの初回描画時のfetch処理が実行されませんでした。
const ScreenA = () => {
useEffect(() => {
// ...fetch処理
}, [])
};
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番上に出てくるようなイメージになります。
そしてこのgoBack()
で画面遷移したときは再レンダリングが行われません。ただ画面が1番上に出てきただけで、新たに読み込まれて表示されるわけではないということです。したがって、遷移したときにuseEffect
は発火しません。
React Routerとの違い
WEB版では画面遷移したタイミングでfetch処理が行われていました。
WEB版の画面遷移の実装は、React RouterのuseNavigate
を使用しています。
引数に遷移する画面のURLを指定して画面遷移します。
このとき、遷移した画面は再レンダリングされるため、画面AのuseEffectは発火したというわけです。
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
useIsFocused
はReact Navigationが提供しているhooksです。
「現在その画面が開かれているか否か?」 を判定し、返り値はbooleanの値です。デフォルトはfalse
ですが、その画面が1番上で開かれた場合はtrue
に変更されます。
このhooksを使えば、画面が最上部に来たことを検知することができます。
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の依存配列に入れました。
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引数に、遷移先の画面に渡したいパラメーターをオブジェクトに入れて指定することができます。
// 遷移先の画面に渡したいパラメーターを指定する
navigation.navigate('Details', {
itemId: 86,
otherParam: 'anything you want here',
});
// 遷移先の画面で渡されたパラメーターを読み取る
const { itemId, otherParam } = route.params;
このパラーメーターに例えばUUIDなどランダムな文字列を生成して指定します。すると、画面遷移するたびに異なるUUIDを生成することができるので、useEffectの依存配列にUUIDを含めれば、変更を検知してuseEffectを発火させることができます。
しかし、useEffectを発火されるためだけにそこまでするか?というのが正直なところなので、今回は採用しませんでした。
まとめ
- React Navigationの
go back()
で画面遷移する場合、再レンダリングは行われない -
useIsFocused
を使うことで画面が最上部に来たことを検知して再レンダリングを行うため、画面遷移したタイミングでuseEffectを発火させることができる
React Navigationでは画面遷移時の動作がReact Routerと異なるため、注意が必要です。
再レンダリングが必要な場合はuseIsFocused
を活用しましょう。
参考