本記事では 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);
}
基本
Navigationする画面とその状態を定義する
まず 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;
画面を定義する
適当に Screen1 と Screen2 のコンポーネントを作成します。react-navigation で管理する画面です。任意のコンポーネントを管理できます。
react-navigation で管理されるコンポーネントは、props に route と navigation を追加する必要があります。これらは自動的に渡されます。
route は画面の名前、パス、パラメータ(ParamList)などの値が入っています。
Route
key | string | route に対する一意のキー。自動で生成される。 |
name | RouteName | route の名前(画面の名前)。 |
path | string | この route のパス。 |
params | object | この route の Param |
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 を生成します。ここでは標準的な Navigator である StackNavigator を生成します。
const Stack = createNativeStackNavigator();
Navigatorに画面を登録する
Navigator に先ほど作成した Screen1 と Screen2 を登録します。
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.Screen の option で cardStyleInterpolator を指定する必要があります。ここでは 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} | 次の画面に対する値 |
index | number | Stackのindex |
closing | Animated.AnimatedInterpolation | スワイプされているかどうか(1: closing, 0: not closing) |
inverted | Animated.AnimatedInterpolation | Animationの動きが変転したときの乗数(-1: inverted, 1: normal) |
layouts | {screen: Layout} | 画面サイズ等 |
StackCardInterpolatedStyle
contanerStyle | any | ContainerのStyle |
cardStyle | any | CardのStyle |
overlayStyle | any | OverlayのStyle |
shadowStyle | any | Shadowの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 | 画面がフォーカスから外れたときに発行される。 |
state | Navigatorの状態が変更されたときに発行されう。 |
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
Router は Navigator の内部で使用されている機能です。各画面の状態を管理し、次に表示する画面を決定します。中身は Reducer です。したがって、Action を dispatch することで状態を更新しています。Navigator を自作する場合は Router の挙動を知っておく必要があるでしょう。
基本的な Action
type | payload |
---|---|
GO_BACK | - |
NAVIGATE | { name: string, params: object} |
RESET | PartialState<NavigationState> |
SET_PARAMS | {params: object} |
Router が管理している状態(NavigationState)
変数名 | 型 | 説明 |
---|---|---|
key | string | 状態に対する一意のキー。自動で生成される。 |
index | number | 現在選択されている route のインデックス。 |
routeNames | keyof ParamList | routeの名前(画面の名前)の配列。 |
history | unknown[] | 履歴 |
routes | NavigationRoute | 描画済みの Route の配列。 |
type | string | Router の種類。stack とか tab とか。 |
stale | false | routerのgetRehydratedStateで状態が復元されたかどうか。なぜ boolean ではない? |
Route
key | string | route に対する一意のキー。自動で生成される。 |
name | RouteName | route の名前(画面の名前)。 |
path | string | この route のパス。 |
params | object | この route の Param |
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
Navigator を自作する
Navigator は Router から与えられた情報を元に画面を描画する役割をになっています。 組み込みの Navigator の挙動に満足できない場合は、Navigator を自作する必要があるでしょう。 とは言っても、そんなに難しく考える必要はありません。Navigatorはただの React のコンポーネントです。
作成手順は次の3ステップにまとめられます。
Navigator を自作手順
- Router を定義する
- useNavigationBuilder を使って NavigatorComponent を定義する
- createNavigatorFactory で Navigator ビルダーを定義する
描画の部分だけを変更したい場合は、Router を作成する必要はありません。ここでは Router を自作する手間を省き、組み込みのStackRouter を使ってみます。
StackRouter の型
type StackRouter<ParamList> = (
options: StackRouterOptions,
) => Router<StackNavigationState<ParamList>, StackActionType>;
NOTE
- StackRouterOptions は DefaultRouterOptions<string> と同じ
- StackNavigationState<ParamList> は NavigationState<ParamList> とほぼ同じ(typeが違う)
useNavigationBuilder
画面を描画するための情報を与えてくれるのが useNavigationBuilder フックです。Router が処理した情報を返してくれます。
useNavigationBuilderの型
useNavigationBuilder: (
createRouter: RouterFactory,
options: DefaultNavigatorOptions & RouterOptions
) => NavigationObject
引数
名前 | 型 | 説明 |
---|---|---|
createRouter | RouterFactory | routerを生成する関数。StackRouter や TabRouter など。 |
options | DefaultNavigatorOptions & RouterOptions | 以下の options を参照。 |
options
名前 | 型 | 説明 |
---|---|---|
children | React.ReactNode | コンポーネントのchildren prop。navigatorで描画されるScreenのリスト。 |
screenOptions | ScreenOptions/(route, navigation) => ScreenOptions | Screenに渡されるScreenOptionのデフォルト値 |
initialRouteName | RouteName | デフォルトで表示されるScreenのroute名前 |
戻り値: NavigationObject
名前 | 型 | 説明 |
---|---|---|
state | State | navigation state。状態によって描画方法を決定する。 |
navigation | Navigation | navigation.dispathでactionを発行する。navigate,goBack,等がある。 |
descriptors | Record<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.Screen の props に渡される 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の定義
NavigatorComponent はただの React のコンポーネントです。 その内部で useNavigationBuilder を使用し Router が処理した情報を受け取ります。 その情報の中で最も大事なものは、どの画面を描画するかです。 描画するまでの手順をまとめると以下になります。
画面描画手順
- routeのインデックスを取得します。 state.index が選択された routeのインデックスです。つまり描画する画面のインデックスです。
- state.routes[state.index] で routeを取得します。
- 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 を使用することはできません。 以下のように Navigator を生成するためのファクトリメソッドの作成する必要があります。
const createMyNavigator = createNavigatorFactory(MyNavigator);
Navigator を使う
では、自作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 の基本的な使い方について説明しました。