Slide 1

Slide 1 text

Expo Router は Expo 導⼊の決め⼿となるか フロントエンドカンファレンス沖縄2023 @Kaito-Dogi

Slide 2

Slide 2 text

自己紹介 @Kaito_Dogi @Kaito-Dogi ❏ 株式会社ゆめみ ❏ Android エンジニア ❏ React Native 挑戦中🔥 ❏ 現地参戦2年⽬😎

Slide 3

Slide 3 text

React Native で モバイル開発したことありますか?

Slide 4

Slide 4 text

フロントエンドエンジニアとの開発で React Native を採⽤した

Slide 5

Slide 5 text

純粋な React Native vs Expo

Slide 6

Slide 6 text

Expo とは ❏ React Native 開発のオープンソースプラットフォーム ❏ Expo SDK を使⽤してネイティブ機能に簡単にアクセスできる ❏ React Native の開発に追従して Expo が開発される ❏ Expo SDK v49.0.0 に対して React Native v0.72 が対応 https://docs.expo.dev/versions/latest/

Slide 7

Slide 7 text

Expo とは メリット デメリット ❏ 開発環境の構築が容易 ❏ アプリのデプロイ‧配布が 簡単になる ❏ React Native の更新に ⼀定の安定性が保証される ❏ Expo SDK に含まれない ネイティブ機能に アクセスできない ❏ アプリサイズが増加する ❏ 最新の React Native を 利⽤できない

Slide 8

Slide 8 text

Expo とは メリット デメリット ❏ 開発環境の構築が容易 ❏ アプリのデプロイ‧配布が 簡単になる ❏ React Native の更新に ⼀定の安定性が保証される ❏ Expo SDK に含まれない ネイティブ機能に アクセスできない ❏ アプリサイズが増加する ❏ 最新の React Native を 利⽤できない

Slide 9

Slide 9 text

Expo とは メリット デメリット ❏ 開発環境の構築が容易 ❏ アプリのデプロイ‧配布が 簡単になる ❏ React Native の更新に ⼀定の安定性が保証される ❏ Expo SDK に含まれない ネイティブ機能に アクセスできない ❏ アプリサイズが増加する ❏ 最新の React Native を 利⽤できない

Slide 10

Slide 10 text

Expo を導⼊してみた ❏ プロジェクトの適性 ❏ Expo で制限されるネイティブ機能を使⽤しない(予定) ❏ ⼩‧中規模なプロジェクト ❏ スピード感を重視 ❏ Android‧フロントエンドエンジニアの学習コスト ❏ 作り込みすぎず、顧客の需要を素早く掴めるか ❏ Expo Router をはじめとした、Expo コミュニティの活発さ https://expo.canny.io/

Slide 11

Slide 11 text

Expo を導⼊してみた ❏ プロジェクトの適性 ❏ Expo で制限されるネイティブ機能を使⽤しない(予定) ❏ 中規模なプロジェクト ❏ スピード感を重視 ❏ Android‧フロントエンドエンジニアの学習コスト ❏ 作り込みすぎず、顧客の需要を素早く掴めるか ❏ Expo Router をはじめとした、Expo コミュニティの活発さ https://expo.canny.io/

Slide 12

Slide 12 text

本セッションで話すこと‧話さないこと ❏ 話すこと ❏ Expo Router の基本的な使い⽅ ❏ React Navigation(後述)との共通点‧相違点 ❏ Expo Router を導⼊して感じたメリット ❏ 話さないこと ❏ React Navigation から Expo Router への移⾏⽅法 ❏ React Native や Expo そのものの込み⼊った話

Slide 13

Slide 13 text

本セッションで話すこと‧話さないこと ❏ 話すこと ❏ Expo Router の基本的な使い⽅ ❏ React Navigation(後述)との共通点‧相違点 ❏ Expo Router を導⼊して感じたメリット ❏ 話さないこと ❏ React Navigation から Expo Router への移⾏⽅法 ❏ React Native や Expo そのものの込み⼊った話

Slide 14

Slide 14 text

本セッションで話すこと‧話さないこと ❏ 話すこと ❏ Expo Router の基本的な使い⽅ ❏ React Navigation(後述)との共通点‧相違点 ❏ Expo Router を導⼊して感じたメリット ❏ 話さないこと ❏ React Navigation から Expo Router への移⾏⽅法 ❏ React Native や Expo そのものの込み⼊った話

Slide 15

Slide 15 text

❏ React Native の新しい画⾯遷移ライブラリ ❏ ファイルシステムベースルーティングが採⽤されている ❏ React Navigation 上に構築されている ❏ React Native の画⾯遷移ライブラリ ❏ Expo SDK v49 に対して Expo Router v2 を使⽤できる ❏ Expo SDK v50 に対して Expo Router v3 が予定されている Expo Router とは

Slide 16

Slide 16 text

❏ React Native の新しい画⾯遷移ライブラリ ❏ ファイルシステムベースルーティングが採⽤されている ❏ React Navigation 上に構築されている ❏ React Native の画⾯遷移ライブラリ ❏ Expo SDK v49 に対して Expo Router v2 を使⽤できる ❏ Expo SDK v50 に対して Expo Router v3 が予定されている Expo Router とは

Slide 17

Slide 17 text

Expo Router の特徴 ❏ スクリーンの描画 ❏ そのディレクトリの _layout.tsx がまず描画される ❏ 次にそのディレクトリの index.tsx が描画される ❏ ディレクトリ構成をもとに、Stack、Tabs が⾃動的に構築される ❏ コンポーネント、router オブジェクト、useRouter で画⾯遷移 ❏ “/hoge” は “app/hoge.tsx”、“app/events/hoge.tsx” ❏ どの画⾯も⾃動でディープリンク可能に(マッピング不要) https://docs.expo.dev/routing/introduction/

Slide 18

Slide 18 text

チケット管理アプリを題材に🎟

Slide 19

Slide 19 text

https://front-okinawa.connpass.com/ Expo Router React Navigation

Slide 20

Slide 20 text

Expo Router React Navigation

Slide 21

Slide 21 text

チケット管理アプリ ❏ 2つのタブで構成 ❏ スタックでイベント詳細へ ❏ モーダルで QR コードを表⽰

Slide 22

Slide 22 text

チケット管理アプリ ❏ 2つのタブで構成 ❏ スタックでイベント詳細へ ❏ モーダルで QR コードを表⽰

Slide 23

Slide 23 text

チケット管理アプリ ❏ 2つのタブで構成 ❏ スタックでイベント詳細へ ❏ モーダルで QR コードを表⽰

Slide 24

Slide 24 text

チケット管理アプリ ❏ 2つのタブで構成 ❏ スタックでイベント詳細へ ❏ モーダルで QR コードを表⽰

Slide 25

Slide 25 text

❏ パッケージのインストール ❏ 設定の変更 ❏ エントリーポイントの変更 ❏ app.json に scheme を追加 ❏ babel.config.js の plugins に追加 ❏ App.tsx を削除 Expo Router のセットアップ https://docs.expo.dev/routing/installation/

Slide 26

Slide 26 text

❏ “expo-router” をインストール ❏ Expo Router セットアップ済みのテンプレートも パッケージのインストール % npx expo install expo-router Terminal % npx expo install expo-router Terminal

Slide 27

Slide 27 text

エントリーポイントの変更 - "main": "node_modules/expo/AppEntry.js", + "main": "expo-router/entry", package.json

Slide 28

Slide 28 text

❏ ディープリンクで使⽤される app.json に scheme を追加 { ..., + "scheme": "expo-router-sample", } app.json

Slide 29

Slide 29 text

module.exports = function (api) { api.cache(true); return { presets: ["babel-preset-expo"], plugins: [ ..., + "expo-router/babel" ], }; }; babel.config.js の plugins に追加 babel.config.js

Slide 30

Slide 30 text

Layout routes ❏ そのディレクトリの _layout.tsx に記述する ❏ ページ共通で描画したいコンポーネントを描画できる ❏ Header、Footer、Context API など import { Slot } from "expo-router"; export default function Layout() { return ; } app/_layout.tsx https://docs.expo.dev/routing/layouts/

Slide 31

Slide 31 text

Layout routes import { Tabs } from "expo-router"; import { TicketProvider } from "@/src/contexts/Ticket"; export default function Layout() { return ( ... ); } app/_layout.tsx 購⼊済みチケットを注⼊

Slide 32

Slide 32 text

❏ Navigator の外側に配置する import { TicketProvider } from "./src/contexts/Ticket"; import { AppNavigator } from "./src/navigation"; export default function App() { return ( ); } 共通レイアウトの配置 App.tsx 購⼊済みチケットを注⼊

Slide 33

Slide 33 text

❏ 画⾯が重なっていくような画⾯遷移 ❏ React Navigation の Native Stack Navigator をラップしている ❏ そのディレクトリの index.tsx がまず描画される Stack https://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

Slide 34

Slide 34 text

Stack ❏ コンポーネントでスタック全体の設定 ❏ コンポーネントで画⾯ごとの設定 import { Stack } from "expo-router"; export default function Layout() { return ( ... ); } app/events/_layout.tsx

Slide 35

Slide 35 text

import { createNativeStackNavigator } from "@react-navigation/native-stack"; export type EventStackParamList = { EventList: undefined; ... }; const Stack = createNativeStackNavigator(); export const EventStack: FC = () => { return ( ... ); }; Native Stack Navigator navigation/EventStack.tsx

Slide 36

Slide 36 text

import { createNativeStackNavigator } from "@react-navigation/native-stack"; export type EventStackParamList = { EventList: undefined; ... }; const Stack = createNativeStackNavigator(); export const EventStack: FC = () => { return ( ... ); }; Native Stack Navigator navigation/EventStack.tsx そのスタックの画⾯を 型として定義

Slide 37

Slide 37 text

import { createNativeStackNavigator } from "@react-navigation/native-stack"; export type EventStackParamList = { EventList: undefined; ... }; const Stack = createNativeStackNavigator(); export const EventStack: FC = () => { return ( ... ); }; Native Stack Navigator navigation/EventStack.tsx Navigator を初期化

Slide 38

Slide 38 text

import { createNativeStackNavigator } from "@react-navigation/native-stack"; export type EventStackParamList = { EventList: undefined; ... }; const Stack = createNativeStackNavigator(); export const EventStack: FC = () => { return ( ... ); }; Native Stack Navigator navigation/EventStack.tsx 各画⾯に対して Component を指定

Slide 39

Slide 39 text

import { createNativeStackNavigator } from "@react-navigation/native-stack"; export type EventStackParamList = { EventList: undefined; ... }; const Stack = createNativeStackNavigator(); export const EventStack: FC = () => { return ( ... ); }; Native Stack Navigator navigation/EventStack.tsx 最初に描画する 画⾯を指定

Slide 40

Slide 40 text

Dynamic routes ❏ [id].tsx で任意の id にマッチできる ❏ useLocalSearchParams でパラメータを取得できる https://docs.expo.dev/routing/create-pages/#dynamic-routes https://docs.expo.dev/router/reference/hooks/#uselocalsearchparams import { Redirect, useLocalSearchParams } from "expo-router"; export default function Page() { const { id } = useLocalSearchParams(); if (typeof id !== "string") return ; return ; } app/events/[id].tsx

Slide 41

Slide 41 text

❏ コンポーネントでは、Href オブジェクトで pathname、params を指定する import { Link } from 'expo-router'; export default function Page() { ... ... ... } Dynamic routes https://docs.expo.dev/routing/navigating-pages/#linking-to-dynamic-routes app/index.tsx

Slide 42

Slide 42 text

❏ route オブジェクトの params から取得する import { RouteProp } from "@react-navigation/native"; type Props = { route: RouteProp; }; export const EventDetailScreen: FC = ({ route }) => { const { id } = route.params; ... Passing parameters to routes https://reactnavigation.org/docs/params/ components/screens/EventDetailScreen/EventDetailScreen.tsx

Slide 43

Slide 43 text

❏ パラメータの型を定義する ❏ パラメータが不要の場合は undefined とする import { createNativeStackNavigator } from "@react-navigation/native-stack"; type EventStackParamList = { EventList: undefined; EventDetail: { id: string }; }; const Stack = createNativeStackNavigator(); Passing parameters to routes https://reactnavigation.org/docs/typescript/#type-checking-screens navigation/EventStack.tsx

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Tabs ❏ コンポーネントでスタック全体の設定 ❏ コンポーネントで画⾯ごとの設定 import { Tabs } from "expo-router"; export default function Layout() { return ( ); } app/_layout.tsx

Slide 46

Slide 46 text

Tabs export default function Layout() { return ( ( ), }} /> ... ); } app/_layout.tsx

Slide 47

Slide 47 text

Tabs export default function Layout() { return ( ( ), }} /> ... ); } app/_layout.tsx 選択されたタブの アイコン‧タイトルの⾊

Slide 48

Slide 48 text

Tabs export default function Layout() { return ( ( ), }} /> ... ); } app/_layout.tsx タブのタイトル

Slide 49

Slide 49 text

Tabs export default function Layout() { return ( ( ), }} /> ... ); } app/_layout.tsx タブのアイコン

Slide 50

Slide 50 text

Tabs ❏ href で遷移する画⾯を明⽰的に指定 ❏ href を null にすると、タブとして表⽰されない import { Tabs } from "expo-router"; export default function Layout() { return ( ); } app/_layout.tsx

Slide 51

Slide 51 text

import { Tabs } from "expo-router"; export default function Layout() { return ( ); } Tabs ❏ href で遷移する画⾯を選択 ❏ href を null にすると、タブとして表⽰されない app/_layout.tsx

Slide 52

Slide 52 text

❏ href で遷移する画⾯を選択 ❏ href を null にすると、タブとして表⽰されない import { Tabs } from "expo-router"; export default function Layout() { return ( ); } Tabs app/_layout.tsx

Slide 53

Slide 53 text

import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; type RootTabParamList = { Event: undefined; ... }; const Tab = createBottomTabNavigator(); export const RootTab: FC = () => { return ( ... ); }; Bottom Tabs navigation/RootTab.tsx

Slide 54

Slide 54 text

import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; type RootTabParamList = { Event: undefined; ... }; const Tab = createBottomTabNavigator(); export const RootTab: FC = () => { return ( ... ); }; Bottom Tabs navigation/RootTab.tsx そのタブの画⾯を型として定義

Slide 55

Slide 55 text

import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; type RootTabParamList = { Event: undefined; ... }; const Tab = createBottomTabNavigator(); export const RootTab: FC = () => { return ( ... ); }; Bottom Tabs navigation/RootTab.tsx Navigator を初期化

Slide 56

Slide 56 text

import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; type RootTabParamList = { Event: undefined; ... }; const Tab = createBottomTabNavigator(); export const RootTab: FC = () => { return ( ... ); }; Bottom Tabs navigation/RootTab.tsx 各画⾯に対して Component を指定

Slide 57

Slide 57 text

import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; type RootTabParamList = { Event: undefined; ... }; const Tab = createBottomTabNavigator(); export const RootTab: FC = () => { return ( ... ); }; Bottom Tabs navigation/RootTab.tsx 最初に描画する 画⾯を指定

Slide 58

Slide 58 text

Issue #763 initialRouteName が動かない https://github.com/expo/router/issues/763

Slide 59

Slide 59 text

Issue #763 initialRouteName が動かない https://github.com/expo/router/issues/763

Slide 60

Slide 60 text

Issue #763 initialRouteName が動かない https://github.com/expo/router/issues/763

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

Modals ❏ の presentation で “modal” を指定する https://docs.expo.dev/router/advanced/modals/ import { Stack } from "expo-router"; export default function Layout() { return ( ... ); } app/_layout.tsx

Slide 63

Slide 63 text

Issue #640 Android で Modals が動かない https://github.com/expo/router/issues/640

Slide 64

Slide 64 text

Issue #640 Android で Modals が動かない ❏ JS Stack Navigator で実装する ❏ Material Design として適切ではないためという意⾒もあり、 Android で Modals を使うかは議論が必要 https://docs.expo.dev/router/advanced/stack/#javascript-stack-with-react-navigationstack https://developer.apple.com/design/human-interface-guidelines/modality

Slide 65

Slide 65 text

❏ 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

Slide 66

Slide 66 text

❏ の presentation で “modal” を指定 Issue #640 Android で Modals が動かない export default function Layout() { return ( ); } app/tickets/_layout.tsx

Slide 67

Slide 67 text

❏ の presentation で “modal” を指定 Issue #640 Android で Modals が動かない export default function Layout() { return ( ); } app/tickets/_layout.tsx

Slide 68

Slide 68 text

Typed routes ❏ 画⾯の絶対パスをユニオン型として⽣成してくれる // prettier-ignore type StaticRoutes = `/` | `/_layout` | `/events/_layout` | `/events/` | `/tickets/_layout` | `/tickets/`; // prettier-ignore type DynamicRoutes = `/events/${SingleRoutePart}` | `/${CatchAllRoutePart}` | `/tickets/${SingleRoutePart}`; // prettier-ignore type DynamicRouteTemplate = `/events/[id]` | `/[...unmatched]` | `/tickets/[id]`; .expo/types/router.d.ts

Slide 69

Slide 69 text

❏ Expo Router v2(Expo SDK v49)では experimental ❏ app.json で Typed routes を有効化 { "expo": { ... + "experiments": { + "typedRoutes": true + } } } app.json Typed routes https://docs.expo.dev/router/reference/typed-routes/

Slide 70

Slide 70 text

❏ ParamList を定義する ❏ その Navigator 内でネストされた Navigator、画⾯の名前を 型付けするのみ(グローバルに遷移できるわけではない) export type EventStackParamList = { EventList: undefined; EventDetail: { id: string }; }; Type checking with TypeScript navigation/EventStack.tsx https://reactnavigation.org/docs/typescript/

Slide 71

Slide 71 text

Unmatched routes ❏ [...unmatched].tsx でカスタマイズできる ❏ レスト構⽂(...)を使⽤していれば名前は⾃由 ❏ “/404” で遷移できる ❏ Expo Router v3 から Not found routes が出る予定 ❏ +not-found.tsx に記述する ❏ ネストされたレベルから全てのルートにマッチ https://docs.expo.dev/routing/error-handling/#unmatched-routes https://docs.expo.dev/router/reference/not-found/

Slide 72

Slide 72 text

Unmatched routes ❏ [...unmatched].tsx でカスタマイズできる ❏ レスト構⽂(...)を使⽤していれば名前は⾃由 ❏ “/404” で遷移できる ❏ Expo Router v3 から Not found routes が出る予定 ❏ +not-found.tsx に記述する ❏ ネストされたレベルから全てのルートにマッチ https://docs.expo.dev/routing/error-handling/#unmatched-routes https://docs.expo.dev/router/reference/not-found/

Slide 73

Slide 73 text

Top-level src directory ❏ src ディレクトリを app に含められる ❏ src/app はルートの app よりも優先される ❏ config ファイル、public ディレクトリはルートに置く ❏ 開発中に移動した場合、キャッシュをクリア https://docs.expo.dev/router/reference/src-directory/ % npx expo start --clear Terminal

Slide 74

Slide 74 text

その他の機能👀

Slide 75

Slide 75 text

❏ グループ構⽂ “()” ❏ URL にセグメントが表⽰されない ❏ app/auth/home.tsx は “app/auth/home” にマッチ ❏ app/(auth)/home.tsx は “app/home” にマッチ ❏ useSegments でグループ名の⽂字列を取得できる Groups https://docs.expo.dev/routing/layouts/#groups https://docs.expo.dev/router/reference/hooks/#usesegments

Slide 76

Slide 76 text

Deep linking ❏ どの画⾯も⾃動でディープリンク可能に ❏ マッピング不要 ❏ その他の実装は従来通り必要(割愛) https://docs.expo.dev/guides/deep-linking/

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

API routes https://blog.expo.dev/rfc-api-routes-cce5a3b9f25d https://github.com/expo/expo/pull/24429 import { 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 版リリース予定

Slide 79

Slide 79 text

Expo Router を使⽤して感じたメリット ❏ ファイルシステムベースルーティングの恩恵 ❏ 型定義やオブジェクトの初期化の必要がなく、簡潔に書ける ❏ Typed routes がないと扱いづらい ❏ Hooks API が便利 ❏ React Navigation では Screen ⽤の Component に ScreenProps (navigation や route オブジェクト)を渡す必要がある ❏ React Navigation との互換性があり、乗り換えやすい

Slide 80

Slide 80 text

まとめ ❏ Expo 導⼊の背景に Expo Router をはじめとした Expo コミュニティの活発さがあった ❏ ファイルシステムベースルーティングを採⽤しており、 簡潔に書けるようになった ❏ Expo Router は React Navigation をラップしており、React Navigation との互換性がある

Slide 81

Slide 81 text

参考記事 ❏ Expo Documentation https://docs.expo.dev/ ❏ React Navigation https://reactnavigation.org/ ❏ Expo Feedback https://expo.canny.io/ ❏ expo/router https://github.com/expo/router ❏ Human Interface Guidelines | Apple Developer Documentation https://developer.apple.com/design/human-interface-guidelines ❏ Material Design https://m3.material.io/ ❏ Evenline - Event Booking App UI Kit https://ui8.net/unpixel/products/evenline---event-booking-app-ui-kit

Slide 82

Slide 82 text

12/09(⼟)に渋⾕で LT &交流会を開催します🎉 https://coopello2.connpass.com/event/301314/

Slide 83

Slide 83 text

ありがとうございました🙌