🔖

Expo Router

2023/05/31に公開

React Nativeでのページ遷移は、React Navigationが使われていました。

const Stack = createNativeStackNavigator();

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Details" component={DetailsScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}
// 公式ドキュメントより

自分はこの記法が苦手で、遷移するためのパス管理をするファイルを用意するのが面倒に感じていました。

そこでNextjsのファイルの構造がそのまま遷移先になるのがとても感動し、Nextjsのルーティングを取り入れたExpo Routerに興味を持ちました。

https://expo.github.io/router/docs/
このドキュメントを読みながら書きます。

環境構築

  • Expo Routerは現在npx expo startコマンドを打つ必要があり、expo無しのpureなReactNativeに使えるかどうかが分かりません。ので、expoを使用します
// expoプロジェクトの作成
// typescriptを使用したいため、--templateを付け、
npx create-expo-app --template

// もしこの時、 Navigationを含めるか?という選択肢が出たらそちらを選択すると、以下の作業が全て必要なくなる

// 依存関係インストール
npx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar react-native-gesture-handler
  • package.jsonを変更
"main": "index.js",
// "node_modules/expo/AppEntry.js" Expoのデフォルトはこちらがエントリー先になっている

// yarn使っていたら以下を追記
{
  "resolutions": {
    "metro": "0.76.0",
    "metro-resolver": "0.76.0"
  }
}
  • index.jsを変更
import "expo-router/entry";
// Routerが用意しているエントリーファイルをインポートするだけ
  • app.json変更
{
  "expo": {
    "scheme": "myapp",

    "web": {
      "bundler": "metro"
    }
  }
}
  • babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ["babel-preset-expo"],
    plugins: [require.resolve("expo-router/babel")],
  };
};
  • startのコマンド
npx expo --clear

Routing

  • Nextjsと同じファイルベースのルーティング規約です
    • appディレクトリに表示したいページを置く必要がある
  • ファイル名がそのままルーティングになる
    • app/home.tsx → /home
    • app/settings/index.tsx → /settings
    • app/[user].tsx → /hiroshi, /tanaka
  • app/blog/[slug].jsは、/blog/123にマッチする
    • もし /blog/bacon.jsが存在し、/blog/baconというパスがあれば先にマッチする

Pageの定義

  • デフォルトエクスポートでReactコンポーネントをエクスポートすることで定義
import { Text } from "react-native";

export default function Home() {
  return <Text>Home page</Text>;
}

Layouts

  • ページ単体は画面いっぱいに表示される
    • ページ間の移動はアニメーション無しの全ページ遷移
  • ネイティブアプリではヘッダーやタブバーなどの共有要素がページ間で持続されるのを期待
  • 複数のページで共通化したいレイアウトは、レイアウトルートを使って作成できる
  • _layout.jsでReactコンポーネントをdefault exportする
  • <Slot />は、選択された子ルートをレンダリングします。このコンポーネントは、他のコンポーネントでラップしてレイアウトを作成することができます。
    • 各ページがSlotの下にレンダリングされるイメージ
import { Slot } from "expo-router";

export default function HomeLayout() {
  return (
    <>
      <Header />
      <Slot />
      <Footer />
    </>
  );
}
  • ネイティブレイアウト
    • Stack: ヘッダーを頂点とするカードのデッキのように画面のスタックをレンダリング
      • ネイティブのアニメーションとジェスチャーをネイティブのスタックナビゲータ、react-navigation/native-stackを拡張
    • Tabs: 下にタブバーがある画面をレンダリング
      • react-navigation/bottom-tabsを拡張
    • Navigator: 画面を一般的でスタイルがないラッパーでレンダリング
      • カスタムレイアウトを作成できる
import { View, Text } from "react-native";
import React from "react";
import { Stack } from "expo-router";

const _layout = () => {
  return <Stack />;
};

export default _layout;

このように_layout.jsxに記載すると、下の方にある画像のようなViewを作成できる。

遷移の仕方

Linkコンポーネント

export default function Home() {
  return (
    <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
      <Stack.Screen options={{ title: "Overview" }} />
      <Link href="/detail">Go to Details</Link>
    </View>
  );
}

<a>タグと同じ感じです。NextjsだとLinkコンポーネントがあるので、同じですね。
これで、Go to Detailsボタンを押せば、app/detail.jsxや、app/detail/indext.jsxのページに遷移できます。

useRouter

import { View, Text } from "react-native";
import React from "react";
import { useRouter } from "expo-router";
const detail = () => {
  const router = useRouter();

  return (
    <View>
      <Text
        onPress={() => {
          router.back();
        }}
      >
        detail
      </Text>
    </View>
  );
};

export default detail;

useRouterを使って呼び出したrouterを使って、
router.push("/profile/settings") などと書いて遷移させたり、
遷移元に戻るように、 router.back() を使います。

Discussion