2023/11/18 開催のフロントエンドカンファレンス沖縄2023にて、『Expo Router は Expo 導入の決め手となるか』 というテーマで発表しました。
Expo Router はExpo 導⼊の決め⼿となるかフロントエンドカンファレンス沖縄2023@Kaito-Dogi
View Slide
自己紹介@Kaito_Dogi@Kaito-Dogi❏ 株式会社ゆめみ❏ Android エンジニア❏ React Native 挑戦中🔥❏ 現地参戦2年⽬😎
React Native でモバイル開発したことありますか?
フロントエンドエンジニアとの開発でReact Native を採⽤した
純粋な React NativevsExpo
Expo とは❏ React Native 開発のオープンソースプラットフォーム❏ Expo SDK を使⽤してネイティブ機能に簡単にアクセスできる❏ React Native の開発に追従して Expo が開発される❏ Expo SDK v49.0.0 に対して React Native v0.72 が対応https://docs.expo.dev/versions/latest/
Expo とはメリット デメリット❏ 開発環境の構築が容易❏ アプリのデプロイ‧配布が簡単になる❏ React Native の更新に⼀定の安定性が保証される❏ Expo SDK に含まれないネイティブ機能にアクセスできない❏ アプリサイズが増加する❏ 最新の React Native を利⽤できない
Expo を導⼊してみた❏ プロジェクトの適性❏ Expo で制限されるネイティブ機能を使⽤しない(予定)❏ ⼩‧中規模なプロジェクト❏ スピード感を重視❏ Android‧フロントエンドエンジニアの学習コスト❏ 作り込みすぎず、顧客の需要を素早く掴めるか❏ Expo Router をはじめとした、Expo コミュニティの活発さhttps://expo.canny.io/
Expo を導⼊してみた❏ プロジェクトの適性❏ Expo で制限されるネイティブ機能を使⽤しない(予定)❏ 中規模なプロジェクト❏ スピード感を重視❏ Android‧フロントエンドエンジニアの学習コスト❏ 作り込みすぎず、顧客の需要を素早く掴めるか❏ Expo Router をはじめとした、Expo コミュニティの活発さhttps://expo.canny.io/
本セッションで話すこと‧話さないこと❏ 話すこと❏ Expo Router の基本的な使い⽅❏ React Navigation(後述)との共通点‧相違点❏ Expo Router を導⼊して感じたメリット❏ 話さないこと❏ React Navigation から Expo Router への移⾏⽅法❏ React Native や Expo そのものの込み⼊った話
❏ React Native の新しい画⾯遷移ライブラリ❏ ファイルシステムベースルーティングが採⽤されている❏ React Navigation 上に構築されている❏ React Native の画⾯遷移ライブラリ❏ Expo SDK v49 に対して Expo Router v2 を使⽤できる❏ Expo SDK v50 に対して Expo Router v3 が予定されているExpo Router とは
Expo Router の特徴❏ スクリーンの描画❏ そのディレクトリの _layout.tsx がまず描画される❏ 次にそのディレクトリの index.tsx が描画される❏ ディレクトリ構成をもとに、Stack、Tabs が⾃動的に構築される❏ コンポーネント、router オブジェクト、useRouter で画⾯遷移❏ “/hoge” は “app/hoge.tsx”、“app/events/hoge.tsx”❏ どの画⾯も⾃動でディープリンク可能に(マッピング不要)https://docs.expo.dev/routing/introduction/
チケット管理アプリを題材に🎟
https://front-okinawa.connpass.com/Expo Router React Navigation
Expo Router React Navigation
チケット管理アプリ❏ 2つのタブで構成❏ スタックでイベント詳細へ❏ モーダルで QR コードを表⽰
❏ パッケージのインストール❏ 設定の変更❏ エントリーポイントの変更❏ app.json に scheme を追加❏ babel.config.js の plugins に追加❏ App.tsx を削除Expo Router のセットアップhttps://docs.expo.dev/routing/installation/
❏ “expo-router” をインストール❏ Expo Router セットアップ済みのテンプレートもパッケージのインストール% npx expo install expo-routerTerminal% npx expo install expo-routerTerminal
エントリーポイントの変更- "main": "node_modules/expo/AppEntry.js",+ "main": "expo-router/entry",package.json
❏ ディープリンクで使⽤されるapp.json に scheme を追加{...,+ "scheme": "expo-router-sample",}app.json
module.exports = function (api) {api.cache(true);return {presets: ["babel-preset-expo"],plugins: [...,+ "expo-router/babel"],};};babel.config.js の plugins に追加babel.config.js
Layout routes❏ そのディレクトリの _layout.tsx に記述する❏ ページ共通で描画したいコンポーネントを描画できる❏ Header、Footer、Context API などimport { Slot } from "expo-router";export default function Layout() {return ;}app/_layout.tsxhttps://docs.expo.dev/routing/layouts/
Layout routesimport { Tabs } from "expo-router";import { TicketProvider } from "@/src/contexts/Ticket";export default function Layout() {return (...);}app/_layout.tsx購⼊済みチケットを注⼊
❏ Navigator の外側に配置するimport { TicketProvider } from "./src/contexts/Ticket";import { AppNavigator } from "./src/navigation";export default function App() {return ();}共通レイアウトの配置App.tsx購⼊済みチケットを注⼊
❏ 画⾯が重なっていくような画⾯遷移❏ React Navigation の Native Stack Navigator をラップしている❏ そのディレクトリの index.tsx がまず描画されるStackhttps://docs.expo.dev/router/advanced/stack/https://reactnavigation.org/docs/native-stack-navigator/import { Stack } from "expo-router";export default function Layout() {return ;}app/_layout.tsx
Stack❏ コンポーネントでスタック全体の設定❏ コンポーネントで画⾯ごとの設定import { Stack } from "expo-router";export default function Layout() {return (...);}app/events/_layout.tsx
import { createNativeStackNavigator } from "@react-navigation/native-stack";export type EventStackParamList = {EventList: undefined;...};const Stack = createNativeStackNavigator();export const EventStack: FC = () => {return (...);};Native Stack Navigatornavigation/EventStack.tsx
import { createNativeStackNavigator } from "@react-navigation/native-stack";export type EventStackParamList = {EventList: undefined;...};const Stack = createNativeStackNavigator();export const EventStack: FC = () => {return (...);};Native Stack Navigatornavigation/EventStack.tsxそのスタックの画⾯を型として定義
import { createNativeStackNavigator } from "@react-navigation/native-stack";export type EventStackParamList = {EventList: undefined;...};const Stack = createNativeStackNavigator();export const EventStack: FC = () => {return (...);};Native Stack Navigatornavigation/EventStack.tsxNavigator を初期化
import { createNativeStackNavigator } from "@react-navigation/native-stack";export type EventStackParamList = {EventList: undefined;...};const Stack = createNativeStackNavigator();export const EventStack: FC = () => {return (...);};Native Stack Navigatornavigation/EventStack.tsx各画⾯に対してComponent を指定
import { createNativeStackNavigator } from "@react-navigation/native-stack";export type EventStackParamList = {EventList: undefined;...};const Stack = createNativeStackNavigator();export const EventStack: FC = () => {return (...);};Native Stack Navigatornavigation/EventStack.tsx最初に描画する画⾯を指定
Dynamic routes❏ [id].tsx で任意の id にマッチできる❏ useLocalSearchParams でパラメータを取得できるhttps://docs.expo.dev/routing/create-pages/#dynamic-routeshttps://docs.expo.dev/router/reference/hooks/#uselocalsearchparamsimport { Redirect, useLocalSearchParams } from "expo-router";export default function Page() {const { id } = useLocalSearchParams();if (typeof id !== "string") return ;return ;}app/events/[id].tsx
❏ コンポーネントでは、Href オブジェクトでpathname、params を指定するimport { Link } from 'expo-router';export default function Page() {...href={{pathname: "/events/[id]",params: { id: "1" }}}>......}Dynamic routeshttps://docs.expo.dev/routing/navigating-pages/#linking-to-dynamic-routesapp/index.tsx
❏ route オブジェクトの params から取得するimport { RouteProp } from "@react-navigation/native";type Props = {route: RouteProp;};export const EventDetailScreen: FC = ({ route }) => {const { id } = route.params;...Passing parameters to routeshttps://reactnavigation.org/docs/params/components/screens/EventDetailScreen/EventDetailScreen.tsx
❏ パラメータの型を定義する❏ パラメータが不要の場合は undefined とするimport { createNativeStackNavigator } from "@react-navigation/native-stack";type EventStackParamList = {EventList: undefined;EventDetail: { id: string };};const Stack = createNativeStackNavigator();Passing parameters to routeshttps://reactnavigation.org/docs/typescript/#type-checking-screensnavigation/EventStack.tsx
Tabs❏ iOS の Tab bars や Android の Navigation bar❏ React Navigation の Bottom Tabs をラップしている❏ そのディレクトリの index.tsx がまず描画されるhttps://docs.expo.dev/router/advanced/tabs/https://reactnavigation.org/docs/bottom-tab-navigator/import { Tabs } from "expo-router";export default function Layout() {return ;}app/_layout.tsx
Tabs❏ コンポーネントでスタック全体の設定❏ コンポーネントで画⾯ごとの設定import { Tabs } from "expo-router";export default function Layout() {return ();}app/_layout.tsx
Tabsexport default function Layout() {return (name="events"options={{title: "イベント",tabBarIcon: ({ color, focused }) => (),}}/>...);}app/_layout.tsx
Tabsexport default function Layout() {return (name="events"options={{title: "イベント",tabBarIcon: ({ color, focused }) => (),}}/>...);}app/_layout.tsx選択されたタブのアイコン‧タイトルの⾊
Tabsexport default function Layout() {return (name="events"options={{title: "イベント",tabBarIcon: ({ color, focused }) => (),}}/>...);}app/_layout.tsxタブのタイトル
Tabsexport default function Layout() {return (name="events"options={{title: "イベント",tabBarIcon: ({ color, focused }) => (),}}/>...);}app/_layout.tsxタブのアイコン
Tabs❏ href で遷移する画⾯を明⽰的に指定❏ href を null にすると、タブとして表⽰されないimport { Tabs } from "expo-router";export default function Layout() {return ();}app/_layout.tsx
import { Tabs } from "expo-router";export default function Layout() {return ();}Tabs❏ href で遷移する画⾯を選択❏ href を null にすると、タブとして表⽰されないapp/_layout.tsx
❏ href で遷移する画⾯を選択❏ href を null にすると、タブとして表⽰されないimport { Tabs } from "expo-router";export default function Layout() {return ();}Tabsapp/_layout.tsx
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";type RootTabParamList = {Event: undefined;...};const Tab = createBottomTabNavigator();export const RootTab: FC = () => {return (...);};Bottom Tabsnavigation/RootTab.tsx
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";type RootTabParamList = {Event: undefined;...};const Tab = createBottomTabNavigator();export const RootTab: FC = () => {return (...);};Bottom Tabsnavigation/RootTab.tsxそのタブの画⾯を型として定義
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";type RootTabParamList = {Event: undefined;...};const Tab = createBottomTabNavigator();export const RootTab: FC = () => {return (...);};Bottom Tabsnavigation/RootTab.tsxNavigator を初期化
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";type RootTabParamList = {Event: undefined;...};const Tab = createBottomTabNavigator();export const RootTab: FC = () => {return (...);};Bottom Tabsnavigation/RootTab.tsx各画⾯に対してComponent を指定
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";type RootTabParamList = {Event: undefined;...};const Tab = createBottomTabNavigator();export const RootTab: FC = () => {return (...);};Bottom Tabsnavigation/RootTab.tsx最初に描画する画⾯を指定
Issue #763 initialRouteName が動かないhttps://github.com/expo/router/issues/763
Issue #763 initialRouteName が動かない❏ 最初に表⽰される index.tsx でリダイレクトhttps://docs.expo.dev/router/reference/redirects/import { Redirect } from "expo-router";export default function Page() {return ;}app/index.tsx
Modals❏ の presentation で “modal” を指定するhttps://docs.expo.dev/router/advanced/modals/import { Stack } from "expo-router";export default function Layout() {return (...);}app/_layout.tsx
Issue #640 Android で Modals が動かないhttps://github.com/expo/router/issues/640
Issue #640 Android で Modals が動かない❏ JS Stack Navigator で実装する❏ Material Design として適切ではないためという意⾒もあり、Android で Modals を使うかは議論が必要https://docs.expo.dev/router/advanced/stack/#javascript-stack-with-react-navigationstackhttps://developer.apple.com/design/human-interface-guidelines/modality
❏ JsStack を初期化import { ParamListBase, StackNavigationState } from "@react-navigation/native";import {createStackNavigator,StackNavigationEventMap,StackNavigationOptions,TransitionPresets,} from "@react-navigation/stack";import { withLayoutContext } from "expo-router";const { Navigator } = createStackNavigator();const JsStack = withLayoutContext<StackNavigationOptions,typeof Navigator,StackNavigationState,StackNavigationEventMap>(Navigator);Issue #640 Android で Modals が動かないapp/tickets/_layout.tsx
❏ の presentation で “modal” を指定Issue #640 Android で Modals が動かないexport default function Layout() {return (name="[id]"options={{...TransitionPresets.ModalPresentationIOS,presentation: "modal",}}/>);}app/tickets/_layout.tsx
Typed routes❏ 画⾯の絶対パスをユニオン型として⽣成してくれる// prettier-ignoretype StaticRoutes = `/` | `/_layout` | `/events/_layout` | `/events/` |`/tickets/_layout` | `/tickets/`;// prettier-ignoretype DynamicRoutes = `/events/${SingleRoutePart}` |`/${CatchAllRoutePart}` | `/tickets/${SingleRoutePart}`;// prettier-ignoretype DynamicRouteTemplate = `/events/[id]` | `/[...unmatched]` |`/tickets/[id]`;.expo/types/router.d.ts
❏ Expo Router v2(Expo SDK v49)では experimental❏ app.json で Typed routes を有効化{"expo": {...+ "experiments": {+ "typedRoutes": true+ }}}app.jsonTyped routeshttps://docs.expo.dev/router/reference/typed-routes/
❏ ParamList を定義する❏ その Navigator 内でネストされた Navigator、画⾯の名前を型付けするのみ(グローバルに遷移できるわけではない)export type EventStackParamList = {EventList: undefined;EventDetail: { id: string };};Type checking with TypeScriptnavigation/EventStack.tsxhttps://reactnavigation.org/docs/typescript/
Unmatched routes❏ [...unmatched].tsx でカスタマイズできる❏ レスト構⽂(...)を使⽤していれば名前は⾃由❏ “/404” で遷移できる❏ Expo Router v3 から Not found routes が出る予定❏ +not-found.tsx に記述する❏ ネストされたレベルから全てのルートにマッチhttps://docs.expo.dev/routing/error-handling/#unmatched-routeshttps://docs.expo.dev/router/reference/not-found/
Top-level src directory❏ src ディレクトリを app に含められる❏ src/app はルートの app よりも優先される❏ config ファイル、public ディレクトリはルートに置く❏ 開発中に移動した場合、キャッシュをクリアhttps://docs.expo.dev/router/reference/src-directory/% npx expo start --clearTerminal
その他の機能👀
❏ グループ構⽂ “()”❏ URL にセグメントが表⽰されない❏ app/auth/home.tsx は “app/auth/home” にマッチ❏ app/(auth)/home.tsx は “app/home” にマッチ❏ useSegments でグループ名の⽂字列を取得できるGroupshttps://docs.expo.dev/routing/layouts/#groupshttps://docs.expo.dev/router/reference/hooks/#usesegments
Deep linking❏ どの画⾯も⾃動でディープリンク可能に❏ マッピング不要❏ その他の実装は従来通り必要(割愛)https://docs.expo.dev/guides/deep-linking/
Deep linking❏ 画⾯とディープリンクをマッピングする必要があるhttps://reactnavigation.org/docs/deep-linking/import { LinkingOptions, NavigationContainer } from "@react-navigation/native";const linking: LinkingOptions = {prefixes: [ ... ],config: {screens: { ... },},};export const AppNavigator: FC = () => {return ( ... );};navigation/AppNavigator.tsx
API routeshttps://blog.expo.dev/rfc-api-routes-cce5a3b9f25dhttps://github.com/expo/expo/pull/24429import { ExpoRequest, ExpoResponse } from 'expo-router/server';export async function POST(req: ExpoRequest): Promise {...return ExpoResponse.json(json);}app/+api.tsx❏ API(サーバーサイドロジック)をプロジェクト内で実装できる❏ +api.ts の接尾辞のファイルで作成❏ HTTP メソッドが⼀致したときに関数が実⾏される❏ Expo Router v3 で beta 版リリース予定
Expo Router を使⽤して感じたメリット❏ ファイルシステムベースルーティングの恩恵❏ 型定義やオブジェクトの初期化の必要がなく、簡潔に書ける❏ Typed routes がないと扱いづらい❏ Hooks API が便利❏ React Navigation では Screen ⽤の Component に ScreenProps(navigation や route オブジェクト)を渡す必要がある❏ React Navigation との互換性があり、乗り換えやすい
まとめ❏ Expo 導⼊の背景に Expo Router をはじめとした Expoコミュニティの活発さがあった❏ ファイルシステムベースルーティングを採⽤しており、簡潔に書けるようになった❏ Expo Router は React Navigation をラップしており、ReactNavigation との互換性がある
参考記事❏ Expo Documentationhttps://docs.expo.dev/❏ React Navigationhttps://reactnavigation.org/❏ Expo Feedbackhttps://expo.canny.io/❏ expo/routerhttps://github.com/expo/router❏ Human Interface Guidelines | Apple Developer Documentationhttps://developer.apple.com/design/human-interface-guidelines❏ Material Designhttps://m3.material.io/❏ Evenline - Event Booking App UI Kithttps://ui8.net/unpixel/products/evenline---event-booking-app-ui-kit
12/09(⼟)に渋⾕で LT &交流会を開催します🎉https://coopello2.connpass.com/event/301314/
ありがとうございました🙌