メインコンテンツまでスキップ

React Native Navigation

· 約19分

本記事では React Native のライブラリ react-navigation の使い方を学びましょう!

インストールと設定

npm i -S @react-navigation/native
npm i -S @react-navigation/native-stack
npm i -S @react-navigation/stack
npm i -S react-native-safe-area-context
npm i -S react-native-screens
npm i -S react-native-gesture-handler

AndroidではMainActivity.javaを編集する必要があります。

import android.os.Bundle;
public class MainActivity extends ReactActivity {

// 以下を追加
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
}

基本

まず react-navigation で管理する画面と各画面のパラメータ(状態)を定義します。ここでは画面を2つ(Screen1, Screen2)を定義しています。このリストを react-navigation では ParamList と呼んでいるようです。

// ParamListの型定義
export type ParamList = typeof initialParamList;

// ParamListの定義
const initialParamList = {
Screen1: {
param1: 1 as number,
param2: 'screen1' as string,
},

Screen2: {
param1: 2 as number,
param2: 'screen2' as string,
},
} as const;

画面の名前のリストを取得する

後で役に立つので画面の名前のリストを定義しておきます。

例えば、routeNames.Screen1 で画面の名前の文字列 "Screen1" を取得できます。

type RouteNames = Readonly<{ [Key in keyof ParamList]: Key }>;

const routeNames = Object.fromEntries(
Object.keys(initialParamList).map(key => [key, key]),
) as RouteNames;

画面を定義する

適当に Screen1Screen2 のコンポーネントを作成します。react-navigation で管理する画面です。任意のコンポーネントを管理できます。

react-navigation で管理されるコンポーネントは、propsroutenavigation を追加する必要があります。これらは自動的に渡されます。

route は画面の名前、パス、パラメータ(ParamList)などの値が入っています。

ヒント

Route

keystringroute に対する一意のキー。自動で生成される。
nameRouteNameroute の名前(画面の名前)。
pathstringこの route のパス。
paramsobjectこの routeParam

navigation はパラメータ(状態)を変更したり、遷移するするための関数を持っています。

ヒント

Navigation

dispatch(action: {type: string, payload: object}) => void状態を変更する
navigate(screen: RouteName, params: ParamList[RouteName]) => void画面遷移をする
reset(state: PartialState) =>void状態をリセットする
goBack() => void前の画面に戻る
isFocused() => boolean画面が選択されているかどうか
icanGoBack() => boolan前の画面に戻れるかどうか
getParent(id?: string) => Navigation親の Navigator を取得する。id が指定されなかった場合は直属の親になる
getState() => State状態を取得する

Screen1

type Screen1Props = StackScreenProps<ParamList, 'Screen1'>;

function Screen1({
route: {params},
navigation,
}: Screen1Props): React.ReactElement {
...
}

Screen2

type Screen2Props = StackScreenProps<ParamList, 'Screen2'>;

function Screen2({
route: {params},
navigation,
}: Screen2Props): React.ReactElement {
...
}

画面を管理する Navigator を生成します。ここでは標準的な Navigator である StackNavigator を生成します。

const Stack = createNativeStackNavigator();

Navigator に先ほど作成した Screen1Screen2 を登録します。

export default function App(): React.ReactElement {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName={routeNames.Screen1}>
<Stack.Screen
name={routeNames.Screen1}
component={Screen1}
initialParams={initialParamList.Screen1}
/>
<Stack.Screen
name={routeNames.Screen2}
component={Screen2}
initialParams={initialParamList.Screen2}
/>
</Stack.Navigator>
</NavigationContainer>
);
}

全ソースコード

ここまでのソースコードをまとめると以下のようになります。

App.tsx

import {NavigationContainer} from '@react-navigation/native';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import {StackScreenProps} from '@react-navigation/stack';
import React from 'react';
import {Button, Text, View} from 'react-native';

const Stack = createNativeStackNavigator();

const initialParamList = {
Screen1: {
param1: 1 as number,
param2: 'screen1' as string,
},

Screen2: {
param1: 2 as number,
param2: 'screen2' as string,
},
} as const;

type ParamList = typeof initialParamList;
type RouteNames = Readonly<{[Key in keyof ParamList]: Key}>;

const routeNames = Object.fromEntries(
Object.keys(initialParamList).map(key => [key, key]),
) as RouteNames;

type Screen1Props = StackScreenProps<ParamList, 'Screen1'>;

function Screen1({
route: {params},
navigation,
}: Screen1Props): React.ReactElement {
return (
<View>
<Text>Screen 1</Text>
<Text>param1: {params.param1}</Text>
<Text>param2: {params.param2}</Text>
<View style={{flexDirection: 'row', justifyContent: 'center'}}>
<Button
title="Go to the Screen2"
onPress={() =>
navigation.navigate(routeNames.Screen2, initialParamList.Screen2)
}
/>
</View>
</View>
);
}

type Screen2Props = StackScreenProps<ParamList, 'Screen2'>;

function Screen2({
route: {params},
navigation,
}: Screen2Props): React.ReactElement {
return (
<View>
<Text>Screen 2</Text>
<Text>param1: {params.param1}</Text>
<Text>param2: {params.param2}</Text>
<View style={{flexDirection: 'row', justifyContent: 'center'}}>
<Button
title="Go to the Screen1"
onPress={() =>
navigation.navigate(routeNames.Screen1, {param1: 10, param2: 'hoge'})
}
/>
</View>
</View>
);
}

export default function App(): React.ReactElement {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName={routeNames.Screen1}>
<Stack.Screen
name={routeNames.Screen1}
component={Screen1}
initialParams={initialParamList.Screen1}
/>
<Stack.Screen
name={routeNames.Screen2}
component={Screen2}
initialParams={initialParamList.Screen2}
/>
</Stack.Navigator>
</NavigationContainer>
);
}

画面遷移アニメーションを変更する

画面の遷移アニメーションを変更するには Stack.ScreenoptioncardStyleInterpolator を指定する必要があります。ここでは horizontalSlideInterpolator という関数を指定しています。

<Stack.Screen
name={routeNames.Screen1}
component={Screen1}
initialParams={initialParamList.Screen1}
options={{
headerShown: false,
cardStyleInterpolator: horizontalSlideInterpolator,
}}
/>

cardStyleInterpolator の型は以下のように定義されています。

ヒント
(props: StackCardInterpolationProps) => StackCardInterpolatedStyle;

StackCardInterpolationProps

current{progress: Animated.AnimatedInterpolation}現在の画面に対する値
next{progress: Animated.AnimatedInterpolation}次の画面に対する値
indexnumberStackのindex
closingAnimated.AnimatedInterpolationスワイプされているかどうか(1: closing, 0: not closing)
invertedAnimated.AnimatedInterpolationAnimationの動きが変転したときの乗数(-1: inverted, 1: normal)
layouts{screen: Layout}画面サイズ等

StackCardInterpolatedStyle

contanerStyleanyContainerのStyle
cardStyleanyCardのStyle
overlayStyleanyOverlayのStyle
shadowStyleanyShadowのStyle

horizontalSlideInterpolator の実装は以下です。画面が右側から現れるアニメーションです。

import { StackCardStyleInterpolator } from '@react-navigation/stack';
import { Animated } from 'react-native';

export const horizontalSlideInterpolator: StackCardStyleInterpolator = ({
current,
next,
inverted,
layouts: { screen },
}) => {
// nextが存在する場合は、currentは1のままで、nextは0から1まで変化する。
// nextが存在しない場合は、currentは0から1まで変化する。
// Animated.addでcurrentとnextを足すと範囲は0から2になる。
// 0から1はこれから表示される画面で、1から2はこれから非表示される画面に対応する。
const progress = Animated.add(
current.progress.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolate: 'clamp',
}),
next
? next.progress.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolate: 'clamp',
})
: 0,
);

return {
cardStyle: {
transform: [
{
translateX: Animated.multiply(
progress.interpolate({
inputRange: [0, 1, 2],
outputRange: [
screen.width, // これから表示される状態。最初は画面外にある。
0, // 画面が完全に表示された状態。
screen.width * -0.5, // 前の画面が完全に非表示された状態。
],
extrapolate: 'clamp',
}),
inverted,
),
},
],
},
};
};

Eventの処理

遷移にともなって発生した Event を処理することができます。

Navigator で管理されている画面のコンポーネント内でイベントハンドラを navigation.addListener で登録することができます。

ヒント

イベントハンドラの登録:

addListener<EventName extends Keyof<EventMap>>(
type: EventName,
callback: EventListenerCallback<EventMap, EventName>
): () => void;

イベントハンドラの削除:

removeListener<EventName extends Keyof<EventMap>>(
type: EventName,
callback: EventListenerCallback<EventMap, EventName
): void;

StackNavigation のイベントには以下が用意されています。

ヒント

StackNavigationEventMap

イベント説明
focus画面がフォーカスされたときに発行される。useFocusEffectを使った方がよい。
blur画面がフォーカスから外れたときに発行される。
stateNavigatorの状態が変更されたときに発行されう。
beforeRemove画面から離れたときには発行される。ユーザが戻るボタンを押した時など。
transitionStart遷移が開始したときに発行される
transitionEnd遷移が終了したときに発行される
gestureStartジェスチャーが開始した時に発行される
gestureEndジェスチャーが終了した時に発行される
gestureCancelジェスチャーが中止された時に発行される

ちなみに、イベントを発行するには自分で Navigator を作成する必要があります。自作の NavigationComponent 内でnavigation.emitをすることでイベントを発行することができます。

戻るボタンの挙動を変える

以下は戻るボタンの挙動を変更する例です。

React.useEffect(() => {
navigation.addListener('beforeRemove', (e) => {
e.preventDefault();

// do something

});
}, [navigation]);

e.preventDefault() でデフォルトの挙動(画面を閉じる等)を中止することができます。その後に代わりの処理を書きます。

もし、デフォルトの挙動を再開したい場合は navigation.dispatch(e.data.action) を使います。

ヒント
e.preventDefault()デフォルトの挙動を中止する。
navigation.dispatch(e.data.action)デフォルトの挙動を再開する。

Router

RouterNavigator の内部で使用されている機能です。各画面の状態を管理し、次に表示する画面を決定します。中身は Reducer です。したがって、Actiondispatch することで状態を更新しています。Navigator を自作する場合は Router の挙動を知っておく必要があるでしょう。

ヒント

基本的な Action

typepayload
GO_BACK-
NAVIGATE{ name: string, params: object}
RESETPartialState<NavigationState>
SET_PARAMS{params: object}
ヒント

Router が管理している状態(NavigationState)

変数名説明
keystring状態に対する一意のキー。自動で生成される。
indexnumber現在選択されている route のインデックス。
routeNameskeyof ParamListrouteの名前(画面の名前)の配列。
historyunknown[]履歴
routesNavigationRoute描画済みの Route の配列。
typestringRouter の種類。stack とか tab とか。
stalefalserouterのgetRehydratedStateで状態が復元されたかどうか。なぜ boolean ではない?
ヒント

Route

keystringroute に対する一意のキー。自動で生成される。
nameRouteNameroute の名前(画面の名前)。
pathstringこの route のパス。
paramsobjectこの routeParam

dispatch は、Reducer のものと同じです。

dispatch: (action: {type: string, payload: object}) => void

'Navigate' という Action で画面遷移を実行する例です。

navigation.dispatch({
type: 'NAVIGATE',
payload: {name: 'ScreenName', params: {...}}
})

'NAVIGATE'のヘルパー関数 navigateが用意されています。

navigate: (screen: RouteName, params: object) => void

NavigatorRouter から与えられた情報を元に画面を描画する役割をになっています。 組み込みの Navigator の挙動に満足できない場合は、Navigator を自作する必要があるでしょう。 とは言っても、そんなに難しく考える必要はありません。Navigatorはただの React のコンポーネントです。

作成手順は次の3ステップにまとめられます。

ヒント
  1. Router を定義する
  2. useNavigationBuilder を使って NavigatorComponent を定義する
  3. createNavigatorFactoryNavigator ビルダーを定義する

描画の部分だけを変更したい場合は、Router を作成する必要はありません。ここでは Router を自作する手間を省き、組み込みのStackRouter を使ってみます。

ヒント

StackRouter の型

type StackRouter<ParamList> = (
options: StackRouterOptions,
) => Router<StackNavigationState<ParamList>, StackActionType>;

NOTE

  • StackRouterOptionsDefaultRouterOptions<string> と同じ
  • StackNavigationState<ParamList>NavigationState<ParamList> とほぼ同じ(typeが違う)

useNavigationBuilder

画面を描画するための情報を与えてくれるのが useNavigationBuilder フックです。Router が処理した情報を返してくれます。

ヒント

useNavigationBuilderの型

useNavigationBuilder: (
createRouter: RouterFactory,
options: DefaultNavigatorOptions & RouterOptions
) => NavigationObject

引数

名前説明
createRouterRouterFactoryrouterを生成する関数。StackRouterTabRouter など。
optionsDefaultNavigatorOptions & RouterOptions以下の options を参照。

options

名前説明
childrenReact.ReactNodeコンポーネントのchildren prop。navigatorで描画されるScreenのリスト。
screenOptionsScreenOptions/(route, navigation) => ScreenOptionsScreenに渡されるScreenOptionのデフォルト値
initialRouteNameRouteNameデフォルトで表示されるScreenのroute名前

戻り値: NavigationObject

名前説明
stateStatenavigation state。状態によって描画方法を決定する。
navigationNavigationnavigation.dispathでactionを発行する。navigate,goBack,等がある。
descriptorsRecord<string, Discriptor>descriptors[route.key].render()でScreenを描画する。

以下のように使います。基本的には、使用する Router といくつかのオプションを渡すだけでOKです。

const { state, navigation, descriptors, NavigationContent } =
useNavigationBuilder<
StackNavigationState<ParamList>,
StackRouterOptions,
StackActionHelpers<ParamList>,
ScreenOptions,
StackNavigationEventMap
>(StackRouter as StackRouter, {
children,
screenOptions: screenOptions as ScreenOptions,
initialRouteName,
});

ここでScreenOptionsは、Navigator.Screenprops に渡される options です。 その値は、descriptors[route.key].options で取得できます。

参考

TabRouterでNavigationを作る場合

useNavigationBuilder の型定義って結構複雑ですよね。参考までに TabRouter の場合の型を載せておきます。

type _TabRouter = (
options: TabRouterOptions,
) => Router<TabNavigationState<ParamList>, TabActionType>;

const { state, navigation, descriptors, NavigationContent } =
useNavigationBuilder<
TabNavigationState<ParamList>,
TabRouterOptions,
TabActionHelpers<ParamList>,
BottomTabNavigationOptions,
BottomTabNavigationEventMap
>(TabRouter as _TabRouter, {
children,
screenOptions: screenOptions as BottomTabNavigationOptions,
initialRouteName,
backBehavior: 'history',
});

NavigatorComponent はただの React のコンポーネントです。 その内部で useNavigationBuilder を使用し Router が処理した情報を受け取ります。 その情報の中で最も大事なものは、どの画面を描画するかです。 描画するまでの手順をまとめると以下になります。

ヒント

画面描画手順

  1. routeのインデックスを取得します。 state.index が選択された routeのインデックスです。つまり描画する画面のインデックスです。
  2. state.routes[state.index]routeを取得します。
  3. descriptors[route.key]?.render() で画面を描画します。

以下は選択された画面を描画する擬似コードです。遷移アニメーションなど何も実装していないので、単に選択された画面に切り替わるだけです。

export function MyNavigator({
initialRouteName,
children,
screenOptions,
}: DefaultNavigatorOptions<
ParamList,
StackNavigationState<ParamList>,
ScreenOptions,
EventMapBase
>): React.ReactElement {
const {...} = useNavigationBuilder(...);

// 選択されたrouteの取得
const route = state.routes[state.index];

return (
<NavigationContent>
<View
style={{
flex: 1,
backgroundColor: 'white',
}}
>
{descriptors[route.key]?.render()}
</View>
</NavigationContent>
);
}

上記で自作の Navigator を定義しましたが、これだけではこの Navigator を使用することはできません。 以下のように Navigator を生成するためのファクトリメソッドの作成する必要があります。

const createMyNavigator = createNavigatorFactory(MyNavigator);

では、自作Navigator を使ってみましょう。組み込みの *StackNavigator の使い方と同じです。

const My = createMyNavigator<ParamList>();

export function MyNavigatorScreens(): React.ReactElement {
return (
<My.Navigator initialRouteName={'Screen1'}>
<My.Screen
name={'Screen1'}
component={Screen1}
initialParams={initialParamList.Screen1}
options={{
title: 'screen1',
}}
/>
<My.Screen
name={'Screen2'}
component={Screen2}
initialParams={initialParamList.Screen2}
options={{
title: 'screen2',
}}
/>
</MyNavigator>
);
}

以上、今回は react-navigation の基本的な使い方について説明しました。