tag:crieit.net,2005:https://crieit.net/tags/ReactNavigation/feed 「ReactNavigation」の記事 - Crieit Crieitでタグ「ReactNavigation」に投稿された最近の記事 2021-10-18T09:38:53+09:00 https://crieit.net/tags/ReactNavigation/feed tag:crieit.net,2005:PublicArticle/17703 2021-10-12T09:52:01+09:00 2021-10-18T09:38:53+09:00 https://crieit.net/posts/React-Navigation-Typescript React NavigationをTypescriptと一緒に使う際につまづいたところ <p>最近業務でReact Nativeを書いているのですが、ナビゲーションのライブラリとして<br /> React NavigationをTypescriptと一緒に使う際に色々大変だったので(主にドキュメントを読んでいないせい)<br /> まとめておこうと思いこの記事を書いています。質問や気になる点があれば<a target="_blank" rel="nofollow noopener" href="https://twitter.com/kmgk21444557">@kmgk21444557</a>までご連絡ください。</p> <p>アプリの作成、実行にはExpoを使っています。</p> <p>サンプルアプリのリポジトリ:<a target="_blank" rel="nofollow noopener" href="https://github.com/kmgk/ReactNavigationExample">https://github.com/kmgk/ReactNavigationExample</a></p> <h2 id="環境"><a href="#%E7%92%B0%E5%A2%83">環境</a></h2> <pre><code>node: v16.10.0 yarn: 1.22.4 expo-cli: 4.12.1 OS: android (Pixel 3a XL API R) </code></pre> <p><code>package.json</code></p> <pre><code class="json">{ "dependencies": { "@react-navigation/bottom-tabs": "^6.0.7", "@react-navigation/native-stack": "^6.2.2", "expo": "~42.0.1", "expo-status-bar": "~1.0.4", "react": "16.13.1", "react-dom": "16.13.1", "react-native": "https://github.com/expo/react-native/archive/sdk-42.0.0.tar.gz", "react-native-safe-area-context": "3.2.0", "react-native-screens": "~3.4.0", "react-native-web": "~0.13.12" }, "devDependencies": { "@babel/core": "^7.9.0", "@react-navigation/native": "^6.0.4", "@types/react": "~16.9.35", "@types/react-native": "~0.63.2", "typescript": "~4.0.0" }, } </code></pre> <h2 id="テスト用アプリの作成"><a href="#%E3%83%86%E3%82%B9%E3%83%88%E7%94%A8%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E4%BD%9C%E6%88%90">テスト用アプリの作成</a></h2> <p>expo-cliを用いてアプリを作成します。テンプレートは<code>blank (Typescript)</code>を選択しました。</p> <pre><code>$ expo init ReactNavigationExample </code></pre> <p>必要なパッケージをインストール</p> <pre><code>yarn add @react-navigation/native @react-navigation/native-stack @react-navigation/bottom-tabs expo install react-native-screens react-native-safe-area-context react-native-pager-view </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://reactnavigation.org/docs/hello-react-navigation">Hello React Navigation</a>を参考に、<br /> <code>HomeScreen</code>と<code>HogeScreen</code>の2画面作成し、<code>createNativeStackNavigator</code>でナビゲーションを作成します。</p> <p><code>App.tsx</code></p> <pre><code class="tsx">import { NavigationContainer, useNavigation } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import React from 'react'; import { Button, StyleSheet, Text, View } from 'react-native'; const HomeStack = createNativeStackNavigator(); export default function App() { return ( <NavigationContainer> <HomeStack.Navigator initialRouteName="Home"> <HomeStack.Screen name="Home" component={HomeScreen} /> <HomeStack.Screen name="Hoge" component={HogeScreen} /> </HomeStack.Navigator> </NavigationContainer> ); } const HomeScreen: React.FC = () => { const navigation = useNavigation() return ( <View style={styles.container}> <Text>HomeScreen</Text> <Button title="Go To HogeScreen" onPress={() => navigation.navigate('Hoge')} /> </View> ) } const HogeScreen: React.FC = () => { return ( <View style={styles.container}> <Text>HogeScreen</Text> </View> ) } const styles = StyleSheet.create({ ... }); </code></pre> <h2><code>navigate('Hoge')</code>でエラーが出る</h2> <p>まずここでエラーが出ます。<code>onPress={() => navigation.navigate('Hoge')}</code>のnavigateの引数である文字列Hogeの型が正しくないようです。</p> <pre><code>型 'string' の引数を型 '{ key: string; params?: undefined; merge?: boolean | undefined; } | { name: never; key?: string | undefined; params: never; merge?: boolean | undefined; }' のパラメーターに割り当てることはできません </code></pre> <p>'Hoge'が割り当てられる予定のnameパラメータの型がneverになっています。なんで。</p> <p>ここで公式に<a target="_blank" rel="nofollow noopener" href="https://reactnavigation.org/docs/typescript">Type checking with TypeScript</a>というページがあることに気が付きます。</p> <blockquote> <p>To type check our route name and params, the first thing we need to do is to create an object type with mappings for route name to the params of the route. For example, say we have a route called Profile in our root navigator which should have a param userId:<br /> とあるのでとりあえずルートをまとめたオブジェクト<code>RootStackParamList</code>を作成します。</p> </blockquote> <pre><code class="tsx">type RootStackParamList = { Home: undefined; Hoge: undefined; } </code></pre> <p>型にundefinedを指定するとナビゲーションからパラメータを与えられません。逆にパラメータが欲しい場合はそのパラメータのマッピングを指定する必要があります。</p> <p>これを<code>createNativeStackNavigator</code>に指定します。</p> <pre><code>const HomeStack = createNativeStackNavigator<RootStackParamList>(); </code></pre> <p>こうすることでNavigator内部のScreenでnameが制限されます。<br /> 定義したルート名しか使えないのでtypoの心配もなくなりました。</p> <p><img src="https://github.com/kmgk/blog/blob/main/static/images/stack-screen-strict-name.png?raw=true" alt="Navigator内部のScreenでnameが制限" /></p> <p>ただ、まだ<code>navigate('Hoge')</code>のエラーは出たままです。先程の記事の<a target="_blank" rel="nofollow noopener" href="https://reactnavigation.org/docs/typescript#type-checking-screens">Type checking screens</a>を見ると</p> <blockquote> <p>To type check our screens, we need to annotate the navigation prop and the route prop received by a screen. The navigator packages in React Navigation export a generic types to define types for both the navigation and route props from the corresponding navigator.<br /> The type takes 2 generics, the param list object we defined earlier, and the name of the current route. This allows us to type check route names and params which you're navigating using navigate, push etc. The name of the current route is necessary to type check the params in route.params and when you call setParams.<br /> また、<code>useNavigation</code>をよく見てみると</p> </blockquote> <pre><code class="tsx">useNavigation<NavigationProp<ReactNavigation.RootParamList, ... </code></pre> <p>とあるので、いい感じにジェネリクスを指定します。今回は<code>createNativeStackNavigator</code>で<br /> StackNavigatorを作成したので<code>NativeStackNavigationProp</code>を使用します。</p> <pre><code class="tsx">const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList, 'Home'>>(); </code></pre> <p>こうすることでエラーは消え、定義したルート名のみが指定できるようになりました。問題解決!</p> <h2 id="Bottom Tabs Navigator でヘッダーが二重に出る問題"><a href="#Bottom+Tabs+Navigator+%E3%81%A7%E3%83%98%E3%83%83%E3%83%80%E3%83%BC%E3%81%8C%E4%BA%8C%E9%87%8D%E3%81%AB%E5%87%BA%E3%82%8B%E5%95%8F%E9%A1%8C">Bottom Tabs Navigator でヘッダーが二重に出る問題</a></h2> <p>まずは<a target="_blank" rel="nofollow noopener" href="https://reactnavigation.org/docs/bottom-tab-navigator">Bottom Tabs Navigator</a>を<br /> 参考にBottom Tabsを用意します。</p> <pre><code class="tsx">type TabParamList = { Tab1: undefined; Tab2: undefined; Tab3: undefined; } const TabNavigator: React.FC = () => { return ( <Tab.Navigator initialRouteName="Tab1"> <Tab.Screen name="Tab1" component={Tab1Screen} /> <Tab.Screen name="Tab2" component={Tab2Screen} /> <Tab.Screen name="Tab3" component={Tab3Screen} /> </Tab.Navigator> ) } // Tab1Screenなどは省略・・・ </code></pre> <p>先程のホーム画面から<code>TabNavigator</code>へ遷移するよう設定し、いざ遷移!</p> <p><img src="https://github.com/kmgk/blog/blob/main/static/images/double-header.png?raw=true" alt="ヘッダーが二重になっている" /></p> <p>なぜかヘッダーが二重になっています。まずは下のヘッダー(タイトルがTab1の方)を消してみます。</p> <h3 id="下のヘッダー(Tab.Screen)を消す"><a href="#%E4%B8%8B%E3%81%AE%E3%83%98%E3%83%83%E3%83%80%E3%83%BC%28Tab.Screen%29%E3%82%92%E6%B6%88%E3%81%99">下のヘッダー(Tab.Screen)を消す</a></h3> <p>これは<code>Tab.Navigator</code>のオプションで<code>headerShown</code>というパラメータがあるのでそれにfalseを渡してあげるだけです。</p> <pre><code class="tsx"><Tab.Navigator initialRouteName="Tab1" screenOptions=<span>{</span><span>{</span>headerShown: false<span>}</span><span>}</span>> </code></pre> <h3 id="上のヘッダー(TabNavigator)を消す"><a href="#%E4%B8%8A%E3%81%AE%E3%83%98%E3%83%83%E3%83%80%E3%83%BC%28TabNavigator%29%E3%82%92%E6%B6%88%E3%81%99">上のヘッダー(TabNavigator)を消す</a></h3> <p>次に上のヘッダー(タイトルがTabの方)を消します。<br /> 先程のTab.Navigatorに渡したheaderShownは一度削除して、再びヘッダーが二重になるようにします。<br /> ここで、TabNavigationを呼び出しているNavigationContainerの方を見てみます。</p> <pre><code class="tsx"><NavigationContainer> <HomeStack.Navigator initialRouteName="Home"> ... <HomeStack.Screen name="Tab" component={TabNavigator} /> </HomeStack.Navigator> </NavigationContainer> </code></pre> <p><code>HomeStack.Screen</code>のヘッダーが表示されているので、それを消せば解決します。<br /> 先程と同様headerShownを渡します。Screenは<code>options</code>でオプションを受け取るので注意。(Navigatorは<code>screenOptions</code>)</p> <pre><code class="tsx"><HomeStack.Screen name="Tab" component={TabNavigator} options=<span>{</span><span>{</span> headerShown: false <span>}</span><span>}</span> /> </code></pre> <p>これで上のヘッダーを削除することができました。ちなみに両方に<code>headerShown: false</code>を渡すとヘッダーがなくなります。当然ですね。</p> <h2 id="参考文献"><a href="#%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE">参考文献</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://reactnavigation.org/docs/typescript">Type checking with TypeScript - reactnavigation.org</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://reactnavigation.org/docs/bottom-tab-navigator#headershown">Bottom Tabs Navigator - reactnavigation.org</a></li> </ul> kmgk