Cover Image for Expo Router を試してみる

Expo Router を試してみる

Expo Router とは、React Native アプリケーションでファイルベースのルーティングを行うためのライブラリです。ハンズオン形式で Expo Router の使い方を学んでいきましょう。また、このハンズオンでは NativeWind を使っています。このライブラリは Tailwind CSS を React Native アプリケーションでも使えるようにするためのものです。

Lesson0 - Construct environment

yeconnect/expo-router-tutorial にこのハンズオンのコードを置いてあります。Lesson ごとにブランチを用意しているので、好きな Lesson から始めることができます。最初から始める場合は以下の手順に従って環境を構築してください。

  1. 上記のリポジトリをクローンします。デフォルトブランチは main ではなく lesson-1 になっています。
  2. プロジェクトディレクトリに移動し、npm install を実行してライブラリをインストールします。
  3. npx expo start を実行して開発サーバーを起動します。

Lesson1 - Create your first page

Contact ページ /contact を作成して、トップページ / との間で行き来できるようにします。Expo Router を導入してあるので、app/contact.js ファイルを作成するだけで自動的に Contact ページが作成されます。app/contact.js を作成して次のように編集してください。

import { View, Text } from "react-native";

export default function Contact() {
  return (
    <View className="flex-1 justify-center items-center">
      <Text className="text-4xl font-bold text-gray-800">
        Contact page
      </Text>
      <Text className="mt-3 text-base font-medium text-blue-500 p-1">
        Go back to Top
      </Text>
    </View>
  );
}

Contact ページは作成されましたが、トップページから移動できない状態です。ナビゲーションを行うには <Link /> コンポーネントを使用し、href プロパティに遷移先のパスを指定します。

index.js 10行目の <Text /> コンポーネントを <Link /> コンポーネントに変更します。Linkexpo-router からインポートする必要があります。

import { Text, View } from "react-native";
import { Link } from "expo-router";

export default function Page() {
  return (
    /* ...rest of the code remains same */
          <Link
            href="/contact"
            className="p-1 text-base text-gray-700 text-blue-500"
          >
            Navigate to Contact
          </Link>
	/* ... */
  );
}

また、contact.js 9行目の <Text /> コンポーネントも同様に変更します。

import { View, Text, StyleSheet } from "react-native";
import { Link } from "expo-router";

export default function Contact() {
  return (
    /* ...rest of the code remains same */
      <Link
        href="/"
        className="mt-3 text-base font-medium text-blue-500 p-1"
      >
        Go back to Top
      </Link>
    /* ... */
  );
}

これで Contact ページとトップページを行き来できるようになりました。

Lesson2 - Dynamic Routes

動的ルートを定義するには [] を使います。例えば、app/fruit/[slug].js/fruit/apple/fruit/banana などにマッチします。ここでは app/member/[slug].js を作成して Member ページとします。app/member/[slug].js を次のように編集してください。

import { View, Text } from "react-native";
import { Link } from "expo-router";

export default function Member() {
  return (
    <View className="flex-1 justify-center items-center">
      <Text className="text-4xl font-bold text-gray-800">
        Member page
      </Text>
      <Text className="mt-3 text-base font-medium text-gray-700">
        Who's profile?
      </Text>
      <Link
        href="/"
        className="mt-3 p-1 text-base font-medium text-blue-500"
      >
        Go back to Top
      </Link>
    </View>
  );
}

トップページから Member ページに移動できるように、index.js を変更します。

export default function Page() {
  return (
    /* ...rest of the code remains same */
        <View className="mt-2 flex-row justify-evenly items-center">
          <Link
            href="/member/CEO"
            className="p-1 text-base text-gray-700 text-blue-500"
          >
            CEO profile
          </Link>
          <Link
            href="/member/CTO"
            className="p-1 text-base text-gray-700 text-blue-500"
          >
            CTO profile
          </Link>
        </View>
    /* ... */
  );
}

これで Member ページに移動できるようになりました。次にパラメータの値を表示するようにします。パラメータの値を取得するには、useSearchParams フックを使います。member/[slug].js を次のように変更します。

import { View, Text, StyleSheet } from "react-native";
import { Link, useSearchParams } from "expo-router";

export default function Member() {
  const params = useSearchParams();

  return (
    /* ...rest of the code remains same */
      <Text className="mt-3 text-base font-medium text-gray-700">
        This is {params.slug}'s profile.
      </Text>
    /* ... */
  );
}

これで Member ページは完成です。

Lesson3 - Layouts

Expo Router を使うと、複数のページによって共有されるコンポーネントを作ることができます。この特殊なコンポーネントは「レイアウト」と言います。レイアウトを共有しているページの間を移動する場合、レイアウトはアンマウントされません。つまり、レイアウトを活用することでページ遷移のコストを小さくすることが出来ます。

レイアウトは _layout.js 内で定義します。_layout.js が置かれているディレクトリ配下のページが影響を受けます。複数のレイアウトが入れ子になることもあります。

レイアウトを使ってアプリ名 "Expo Router Tutorial" を表示するヘッダーを作成してみましょう。すべてのページにレイアウトを適用したいので、app/_layout.js 内でレイアウトを定義します。app/_layout.js を次のように編集してください。

import { View, Text } from "react-native";
import { Slot } from "expo-router";
import { StatusBar } from "expo-status-bar";

export default function Layout() {
  return (
    <>
      <View className="h-24 justify-end bg-gray-100 pl-6 pb-2">
        <Text className="text-3xl text-gray-800 font-bold">Expo Router Tutorial</Text>
      </View>
      <Slot />
      <StatusBar style="dark" />
    </>
  );
}

<Slot /> に各ページの内容が埋め込まれます。これですべてのページからヘッダーが見れるようになりました。また、ステータスバーも style="dark" を指定して見れるようにしました。

Lesson4 - Stack

最後にネイティブアプリらしい UI に挑戦してみましょう。Expo Router が用意している Stack レイアウトを利用すれば、簡単に LINE のトーク画面のような UI を実装することが出来ます。

app/ ディレクトリの構成を次のように変更します。

app
├── (main)
│   ├── _layout.js
│   ├── contact.js
│   ├── index.js
│   └── member
│       └── [slug].js
└── (talk)
    ├── _layout.js
    ├── talklist.js
    └── talkroom.js

これまで app/ 直下にあったものは app/(main)/ 内に移動しています。(main)(talk) のように、名前が () で囲われているディレクトリはパスの一部になりません。この機能は "group" と言います。

(talk)/_layout.js を次のように編集します。

import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";

export default function StackLayout() {
  return (
    <>
      <Stack />
      <StatusBar style="dark" />
    </>
  );
}

(talk)/talklist.js を次のように編集します。

import { View, Text, Pressable } from "react-native";
import { Link, Stack } from "expo-router";
import { AntDesign } from "@expo/vector-icons";

export default function TalkList() {
  return (
    <>
      <Stack.Screen
        options={{
          title: "トーク",
        }}
      />
      <View className="flex-1 justify-start items-stretch">
        <Link href="/talkroom" asChild >
          <Pressable>
            <View className="h-20 py-2 px-4 flex-row items-center">
              <AntDesign name="google" size={48} color="black" />
              <View className="flex-1 pl-3">
                <Text className="text-base font-semibold text-gray-800">Google</Text>
                <Text className="text-sm text-gray-600">ChatGPTに対抗できるAIを...</Text>
              </View>
              <Text
                style={{ fontFamily: "Menlo" }}
                className="self-start pt-3 text-xs text-gray-500"
              >
                0:45
              </Text>
            </View>
          </Pressable>
        </Link>
      </View>
    </>
  );
}

通常 <Link /> コンポーネントは文字列を囲って使います。コンポーネントを囲みたい場合は、このコードのように、asChild 属性を付けて、間に <Pressable /> コンポーネントを挟みます。

(talk)/talkroom.js を次のように編集します。

import { View, Text } from "react-native";
import { Stack } from "expo-router";

export default function TalkRoom() {
  return (
    <>
      <Stack.Screen
        options={{
          title: "Google",
        }}
      />
      <View className="flex-1 justify-center items-center">
        <Text className="text-base font-medium text-gray-600">
          talk room with Google
        </Text>
      </View>
    </>
  );
}

最後にトップページからトーク画面に移動できるよう、(main)/index.js を変更します。

export default function Page() {
  return (
    /* ...rest of the code remains same */
      <View className="mb-5">
        <Text className="text-2xl text-gray-800 font-bold">Lesson4 - Stack</Text>
        <Text className="text-base text-gray-700 mt-3">
          Expo Routerが用意しているStackレイアウトを利用して、LINEのトーク画面のようなUIを実装してみましょう。
        </Text>
        <View className="mt-2 items-center">
          <Link
            href="/talklist"
            className="p-1 text-base text-gray-700 text-blue-500"
          >
            トーク画面に移動する
          </Link>
        </View>
      </View>
    /* ... */
  );
}

スワイプ操作でページを移動できることが確かめられると思います。Expo Router は他にも Tabs レイアウトを用意しています。こちらもネイティブアプリでよく見られる UI を実装したものなので、ぜひ試してみてください。