概要
過去に開発していたmemoirを、最新のコードベースにリニューアル中。その途中経過をまとめる。
アプリはExpoで開発。最新化に伴い以下を修正中。
- Expo SDK 48 → 52にバージョンアップ
- ナビゲーションをReact Navigation → expo-routerに移行
- ディレクトリ構造をAtomic Design → Feature型に変更
- Linter & FormatterをESLint & Prettier → Biomeに移行
PR
最初に試したこと
Expoのバージョンアップは以下のコマンドで可能。
$ npx expo install expo@latest
これでバージョンアップを試みたが、EAS Buildが通らなかった。
EAS Buildが通らないとデバッグができないため、一時的にeas.jsonを削除してExpo Goで起動できるように対応を進めた。
Expo Goでは問題なく起動したため、コードの最新化を兼ねてまずexpo-routerへの移行から実装を開始。
expo-routerとは
Expo Routerは、React Navigationを基盤にしたExpo向けのナビゲーションライブラリで、Next.jsのようなファイルベースのルーティングシステムを採用。
基本的なディレクトリ構造は以下の通り。
./app ├── _layout.tsx // 画面全体の共通処理 ├── index.tsx // ホーム画面 ├── items │ └── [id].tsx // アイテム詳細画面(URLパラメータ対応) ├── my-page │ ├── index.tsx // マイページ画面 │ └── update-profile // プロフィール更新画面 └── search └── index.tsx // 検索画面
実装
認証周りの実装
expo-routerでは、認証関連の構造もファイルベースで以下のように表現可能。
./app ├── (app) │ ├── _layout.tsx // ログイン後の共通処理 │ ├── index.tsx // ログイン後のTOP画面 │ ├── items │ ├── login │ ├── memoir │ ├── my-page │ ├── search │ └── setting ├── _layout.tsx └── sign-in.tsx // ログイン画面
以下に各ファイルを示す。
app/_layout.tsx
import { SessionProvider } from "@/ctx"; import makeApolloClient from "@/lib/apollo"; import { ApolloProvider } from "@apollo/client"; import { ActionSheetProvider } from "@expo/react-native-action-sheet"; import { Slot } from "expo-router"; import { RecoilRoot } from "recoil"; export default function Root() { const client = makeApolloClient(); return ( <ActionSheetProvider> <ApolloProvider client={client}> <RecoilRoot> <SessionProvider> <Slot /> </SessionProvider> </RecoilRoot> </ApolloProvider> </ActionSheetProvider> ); }
app/sign-in.tsx
import Page from "@/features/top/components/Connected"; import useUser from "@/hooks/useUser"; import { NotoSansJP_700Bold } from "@expo-google-fonts/noto-sans-jp"; import { RobotoCondensed_700Bold } from "@expo-google-fonts/roboto-condensed"; import * as Font from "expo-font"; import { useCallback, useState } from "react"; export default function SignIn() { const { user, onSaveWhenNotLogin } = useUser(); const [create, setCreate] = useState(false); const onSkip = useCallback(() => { setCreate(true); onSaveWhenNotLogin(); }, [onSaveWhenNotLogin]); const [fontsLoaded] = Font.useFonts({ "RobotoCondensed-Bold": RobotoCondensed_700Bold, "NotoSansJP-Bold": NotoSansJP_700Bold, }); if (!fontsLoaded) { return null; } return ( <Page onSkip={onSkip} setCreate={setCreate} create={create} isExistUser={!!user.id} /> ); }
app/(app)/_layout.tsx/_layout.tsx)
import { useSession } from "@/ctx"; import { Redirect, Stack } from "expo-router"; import { Text } from "react-native"; export default function AppLayout() { const { session, isLoading } = useSession(); if (isLoading) { return <Text>Loading...</Text>; } if (!session) { return <Redirect href="/sign-in" />; // 未ログイン時にリダイレクト } return <Stack />; }
URLパラメータ、GETパラメータの取得
memoirでは、アイテム詳細画面を以下のように設定。
./app └── (app) └── items └── [id].tsx
アイテム詳細画面では、URLパラメータid以外にも、dateを画面に渡す仕様。
以下のコードで遷移先を指定。
features/home/components/Connected.tsx
const onItem = useCallback( (itemID: string) => { const date = dayjs(homeDate.date).format("YYYY-MM-DD"); router.push({ pathname: `/items/${itemID}`, params: { date, }, }); }, [router, homeDate.date], );
以下でパラメータを受け取る。
features/items/components/index.tsx
import { useLocalSearchParams } from "expo-router"; import type { FC } from "react"; import { memo } from "react"; import Connected from "./Connected"; const ItemDetail: FC = () => { const { id, date } = useLocalSearchParams<{ id: string; date: string }>(); return <Connected date={date} itemID={id} />; }; export default memo(ItemDetail);
まとめ
ここまでの移行作業により、大半の画面は以下の画像のように移行が完了。

残作業として以下を進行予定。
- EAS Buildの復活
- Googleログインの移行
- Deep Linkの実装
Expoは便利だが、最新化を怠るとデバッグもできなくなるため、継続的な対応が必要だと感じた。