Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Kaito-Dogi
November 18, 2023

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

2023/11/18 開催のフロントエンドカンファレンス沖縄2023にて、『Expo Router は Expo 導入の決め手となるか』 というテーマで発表しました。

Kaito-Dogi

November 18, 2023
Tweet

More Decks by Kaito-Dogi

Other Decks in Programming

Transcript

  1. Expo とは ❏ React Native 開発のオープンソースプラットフォーム ❏ Expo SDK を使⽤してネイティブ機能に簡単にアクセスできる

    ❏ React Native の開発に追従して Expo が開発される ❏ Expo SDK v49.0.0 に対して React Native v0.72 が対応 https://docs.expo.dev/versions/latest/
  2. Expo とは メリット デメリット ❏ 開発環境の構築が容易 ❏ アプリのデプロイ‧配布が 簡単になる ❏

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

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

    React Native の更新に ⼀定の安定性が保証される ❏ Expo SDK に含まれない ネイティブ機能に アクセスできない ❏ アプリサイズが増加する ❏ 最新の React Native を 利⽤できない
  5. Expo を導⼊してみた ❏ プロジェクトの適性 ❏ Expo で制限されるネイティブ機能を使⽤しない(予定) ❏ ⼩‧中規模なプロジェクト ❏

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

    スピード感を重視 ❏ Android‧フロントエンドエンジニアの学習コスト ❏ 作り込みすぎず、顧客の需要を素早く掴めるか ❏ Expo Router をはじめとした、Expo コミュニティの活発さ https://expo.canny.io/
  7. 本セッションで話すこと‧話さないこと ❏ 話すこと ❏ Expo Router の基本的な使い⽅ ❏ React Navigation(後述)との共通点‧相違点

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

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

    ❏ Expo Router を導⼊して感じたメリット ❏ 話さないこと ❏ React Navigation から Expo Router への移⾏⽅法 ❏ React Native や Expo そのものの込み⼊った話
  10. ❏ React Native の新しい画⾯遷移ライブラリ ❏ ファイルシステムベースルーティングが採⽤されている ❏ React Navigation 上に構築されている

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

    ❏ React Native の画⾯遷移ライブラリ ❏ Expo SDK v49 に対して Expo Router v2 を使⽤できる ❏ Expo SDK v50 に対して Expo Router v3 が予定されている Expo Router とは
  12. Expo Router の特徴 ❏ スクリーンの描画 ❏ そのディレクトリの _layout.tsx がまず描画される ❏

    次にそのディレクトリの index.tsx が描画される ❏ ディレクトリ構成をもとに、Stack、Tabs が⾃動的に構築される ❏ <Link /> コンポーネント、router オブジェクト、useRouter で画⾯遷移 ❏ “/hoge” は “app/hoge.tsx”、“app/events/hoge.tsx” ❏ どの画⾯も⾃動でディープリンク可能に(マッピング不要) https://docs.expo.dev/routing/introduction/
  13. ❏ パッケージのインストール ❏ 設定の変更 ❏ エントリーポイントの変更 ❏ app.json に scheme

    を追加 ❏ babel.config.js の plugins に追加 ❏ App.tsx を削除 Expo Router のセットアップ https://docs.expo.dev/routing/installation/
  14. module.exports = function (api) { api.cache(true); return { presets: ["babel-preset-expo"],

    plugins: [ ..., + "expo-router/babel" ], }; }; babel.config.js の plugins に追加 babel.config.js
  15. Layout routes ❏ そのディレクトリの _layout.tsx に記述する ❏ ページ共通で描画したいコンポーネントを描画できる ❏ Header、Footer、Context

    API など import { Slot } from "expo-router"; export default function Layout() { return <Slot />; } app/_layout.tsx https://docs.expo.dev/routing/layouts/
  16. Layout routes import { Tabs } from "expo-router"; import {

    TicketProvider } from "@/src/contexts/Ticket"; export default function Layout() { return ( <TicketProvider> <Tabs>...</Tabs> </TicketProvider> ); } app/_layout.tsx 購⼊済みチケットを注⼊
  17. ❏ Navigator の外側に配置する import { TicketProvider } from "./src/contexts/Ticket"; import

    { AppNavigator } from "./src/navigation"; export default function App() { return ( <TicketProvider> <AppNavigator /> </TicketProvider> ); } 共通レイアウトの配置 App.tsx 購⼊済みチケットを注⼊
  18. ❏ 画⾯が重なっていくような画⾯遷移 ❏ 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 <Stack />; } app/_layout.tsx
  19. Stack ❏ <Stack /> コンポーネントでスタック全体の設定 ❏ <Stack.Screen /> コンポーネントで画⾯ごとの設定 import

    { Stack } from "expo-router"; export default function Layout() { return ( <Stack screenOptions={{ headerShown: false }}> <Stack.Screen name="index" /> ... </Stack> ); } app/events/_layout.tsx
  20. import { createNativeStackNavigator } from "@react-navigation/native-stack"; export type EventStackParamList =

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

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

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

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

    { EventList: undefined; ... }; const Stack = createNativeStackNavigator<EventStackParamList>(); export const EventStack: FC = () => { return ( <Stack.Navigator initialRouteName={"EventList"} > <Stack.Screen name={"EventList"} component={EventListScreen} /> ... </Stack.Navigator> ); }; Native Stack Navigator navigation/EventStack.tsx 最初に描画する 画⾯を指定
  25. 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 <Redirect href="/404" />; return <EventDetailScreen id={id} />; } app/events/[id].tsx
  26. ❏ <Link /> コンポーネントでは、Href オブジェクトで pathname、params を指定する import { Link

    } from 'expo-router'; export default function Page() { ... <Link href={{ pathname: "/events/[id]", params: { id: "1" } }}> ... </Link> ... } Dynamic routes https://docs.expo.dev/routing/navigating-pages/#linking-to-dynamic-routes app/index.tsx
  27. ❏ route オブジェクトの params から取得する import { RouteProp } from

    "@react-navigation/native"; type Props = { route: RouteProp<EventStackParamList, "EventDetail">; }; export const EventDetailScreen: FC<Props> = ({ route }) => { const { id } = route.params; ... Passing parameters to routes https://reactnavigation.org/docs/params/ components/screens/EventDetailScreen/EventDetailScreen.tsx
  28. ❏ パラメータの型を定義する ❏ パラメータが不要の場合は undefined とする import { createNativeStackNavigator }

    from "@react-navigation/native-stack"; type EventStackParamList = { EventList: undefined; EventDetail: { id: string }; }; const Stack = createNativeStackNavigator<EventStackParamList>(); Passing parameters to routes https://reactnavigation.org/docs/typescript/#type-checking-screens navigation/EventStack.tsx
  29. 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 <Tabs />; } app/_layout.tsx
  30. Tabs ❏ <Tabs /> コンポーネントでスタック全体の設定 ❏ <Tabs.Screen /> コンポーネントで画⾯ごとの設定 import

    { Tabs } from "expo-router"; export default function Layout() { return ( <Tabs screenOptions={{ headerShown: false }}> <Tabs.Screen name="events" /> </Tabs> ); } app/_layout.tsx
  31. Tabs export default function Layout() { return ( <Tabs screenOptions={{

    tabBarActiveTintColor: colors.primary }} > <Tabs.Screen name="events" options={{ title: "イベント", tabBarIcon: ({ color, focused }) => ( <PeopleIcon color={color as `#${string}`} outline={!focused} /> ), }} /> ... </Tabs> ); } app/_layout.tsx
  32. Tabs export default function Layout() { return ( <Tabs screenOptions={{

    tabBarActiveTintColor: colors.primary }} > <Tabs.Screen name="events" options={{ title: "イベント", tabBarIcon: ({ color, focused }) => ( <PeopleIcon color={color as `#${string}`} outline={!focused} /> ), }} /> ... </Tabs> ); } app/_layout.tsx 選択されたタブの アイコン‧タイトルの⾊
  33. Tabs export default function Layout() { return ( <Tabs screenOptions={{

    tabBarActiveTintColor: colors.primary }} > <Tabs.Screen name="events" options={{ title: "イベント", tabBarIcon: ({ color, focused }) => ( <PeopleIcon color={color as `#${string}`} outline={!focused} /> ), }} /> ... </Tabs> ); } app/_layout.tsx タブのタイトル
  34. Tabs export default function Layout() { return ( <Tabs screenOptions={{

    tabBarActiveTintColor: colors.primary }} > <Tabs.Screen name="events" options={{ title: "イベント", tabBarIcon: ({ color, focused }) => ( <PeopleIcon color={color as `#${string}`} outline={!focused} /> ), }} /> ... </Tabs> ); } app/_layout.tsx タブのアイコン
  35. Tabs ❏ href で遷移する画⾯を明⽰的に指定 ❏ href を null にすると、タブとして表⽰されない import

    { Tabs } from "expo-router"; export default function Layout() { return ( <Tabs> <Tabs.Screen name="events" options={{ href: "events" }} /> <Tabs.Screen name="index" options={{ href: null }} /> </Tabs> ); } app/_layout.tsx
  36. import { Tabs } from "expo-router"; export default function Layout()

    { return ( <Tabs> <Tabs.Screen name="events" options={{ href: "events" }} /> <Tabs.Screen name="index" /> </Tabs> ); } Tabs ❏ href で遷移する画⾯を選択 ❏ href を null にすると、タブとして表⽰されない app/_layout.tsx
  37. ❏ href で遷移する画⾯を選択 ❏ href を null にすると、タブとして表⽰されない import {

    Tabs } from "expo-router"; export default function Layout() { return ( <Tabs> <Tabs.Screen name="events" options={{ href: "events" }} /> <Tabs.Screen name="index" options={{ href: null }} /> </Tabs> ); } Tabs app/_layout.tsx
  38. import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; type RootTabParamList = {

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

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

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

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

    Event: undefined; ... }; const Tab = createBottomTabNavigator<RootTabParamList>(); export const RootTab: FC = () => { return ( <Tab.Navigator initialRouteName="Event" > <Tab.Screen name="Event" component={EventStack} /> ... </Tab.Navigator> ); }; Bottom Tabs navigation/RootTab.tsx 最初に描画する 画⾯を指定
  43. Issue #763 initialRouteName が動かない ❏ 最初に表⽰される index.tsx でリダイレクト https://docs.expo.dev/router/reference/redirects/ import

    { Redirect } from "expo-router"; export default function Page() { return <Redirect href="/events" />; } app/index.tsx
  44. Modals ❏ <Stack.Screen /> の presentation で “modal” を指定する https://docs.expo.dev/router/advanced/modals/

    import { Stack } from "expo-router"; export default function Layout() { return ( <Stack> ... <Stack.Screen name="[id]" options={{ presentation: "modal" }} /> </Stack> ); } app/_layout.tsx
  45. 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
  46. ❏ 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<ParamListBase>, StackNavigationEventMap >(Navigator); Issue #640 Android で Modals が動かない app/tickets/_layout.tsx
  47. ❏ <JsStack.Screen /> の presentation で “modal” を指定 Issue #640

    Android で Modals が動かない export default function Layout() { return ( <JsStack> <JsStack.Screen name="[id]" options={{ ...TransitionPresets.ModalPresentationIOS, presentation: "modal", }} /> </JsStack> ); } app/tickets/_layout.tsx
  48. ❏ <JsStack.Screen /> の presentation で “modal” を指定 Issue #640

    Android で Modals が動かない export default function Layout() { return ( <JsStack> <JsStack.Screen name="[id]" options={{ ...TransitionPresets.ModalPresentationIOS, presentation: "modal", }} /> </JsStack> ); } app/tickets/_layout.tsx
  49. Typed routes ❏ 画⾯の絶対パスをユニオン型として⽣成してくれる // prettier-ignore type StaticRoutes = `/`

    | `/_layout` | `/events/_layout` | `/events/` | `/tickets/_layout` | `/tickets/`; // prettier-ignore type DynamicRoutes<T extends string> = `/events/${SingleRoutePart<T>}` | `/${CatchAllRoutePart<T>}` | `/tickets/${SingleRoutePart<T>}`; // prettier-ignore type DynamicRouteTemplate = `/events/[id]` | `/[...unmatched]` | `/tickets/[id]`; .expo/types/router.d.ts
  50. ❏ 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/
  51. ❏ ParamList を定義する ❏ その Navigator 内でネストされた Navigator、画⾯の名前を 型付けするのみ(グローバルに遷移できるわけではない) export

    type EventStackParamList = { EventList: undefined; EventDetail: { id: string }; }; Type checking with TypeScript navigation/EventStack.tsx https://reactnavigation.org/docs/typescript/
  52. 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/
  53. 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/
  54. Top-level src directory ❏ src ディレクトリを app に含められる ❏ src/app

    はルートの app よりも優先される ❏ config ファイル、public ディレクトリはルートに置く ❏ 開発中に移動した場合、キャッシュをクリア https://docs.expo.dev/router/reference/src-directory/ % npx expo start --clear Terminal
  55. ❏ グループ構⽂ “()” ❏ 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
  56. Deep linking ❏ 画⾯とディープリンクをマッピングする必要がある https://reactnavigation.org/docs/deep-linking/ import { LinkingOptions, NavigationContainer }

    from "@react-navigation/native"; const linking: LinkingOptions<ReactNavigation.RootParamList> = { prefixes: [ ... ], config: { screens: { ... }, }, }; export const AppNavigator: FC = () => { return ( <NavigationContainer linking={linking}> ... </NavigationContainer> ); }; navigation/AppNavigator.tsx
  57. 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<ExpoResponse> { ... return ExpoResponse.json(json); } app/+api.tsx ❏ API(サーバーサイドロジック)をプロジェクト内で実装できる ❏ +api.ts の接尾辞のファイルで作成 ❏ HTTP メソッドが⼀致したときに関数が実⾏される ❏ Expo Router v3 で beta 版リリース予定
  58. Expo Router を使⽤して感じたメリット ❏ ファイルシステムベースルーティングの恩恵 ❏ 型定義やオブジェクトの初期化の必要がなく、簡潔に書ける ❏ Typed routes

    がないと扱いづらい ❏ Hooks API が便利 ❏ React Navigation では Screen ⽤の Component に ScreenProps (navigation や route オブジェクト)を渡す必要がある ❏ React Navigation との互換性があり、乗り換えやすい
  59. まとめ ❏ Expo 導⼊の背景に Expo Router をはじめとした Expo コミュニティの活発さがあった ❏

    ファイルシステムベースルーティングを採⽤しており、 簡潔に書けるようになった ❏ Expo Router は React Navigation をラップしており、React Navigation との互換性がある
  60. 参考記事 ❏ 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