🚀

Next.jsとExpoを組み合わせた最強のクロスプラットフォーム開発環境

2024/09/17に公開

対象読者

  • 一つのコードベースでWebもネイティブも実現したい開発者
  • WebではNext.js、ネイティブではExpoの恩恵を受けたいと考えている方

利用する技術

技術 用途
Next.js Webアプリケーション開発
Expo ネイティブアプリケーション開発
Tamagui コンポーネントをWebとネイティブで共通化
Solito ナビゲーション(ページ遷移)をWebとネイティブで共通化(次回の記事で使い方を書きます)

この構成のメリット

  1. ロジックだけでなく、ViewもWebとNativeで共通化できる
  2. 完全に共通化することも、プラットフォーム固有の機能を利用する場合はその部分だけを分けることも可能
  3. Next.jsとExpoで同様の方法でルーティングが可能(Pages Router、Expo Router)

React NativeコンポーネントとNext.jsの課題

React NativeはWebとネイティブアプリの開発を同じ言語で行えるという利点で注目されてきました。
しかし、Viewの完全な共通化には課題がありました。

課題点

  1. React Nativeコンポーネント(react-nativeパッケージ)はNext.jsのビルドに対応していない
  2. Next.jsのSSR、SSGなどの高度な機能が使えない

この課題を解決するのが、Tamaguiです。

Tamagui

Tamaguiは、「ロジックだけでなく、Viewも共通化しつつ、Next.jsの恩恵も受けたい」という開発者の強欲を叶えてくれるフレームワークです。

Tamaguiの特徴

  • ビルド時にプラットフォームごとに適切なコードに変換
  • Webの場合はdivタグとCSS、Nativeの場合はreact-nativeViewコンポーネントを出力
  • Next.jsでのビルドが可能 -> SSRやSSGといった機能が使えるようになる

Tamaguiの使用例

import { Button } from 'tamagui'

export default function MyButton({ title, onPress }) {
  return (
    <Button
      backgroundColor="$blue10"
      color="white"
      paddingVertical={10}
      paddingHorizontal={20}
      onPress={onPress}
    >
      {title}
    </Button>
  )
}

完全な共通化が難しい場合の対処法

Tamaguiを使用しても、一部のサードパーティライブラリやWeb固有の機能は共通化が難しい場合があります。そんな時は、Expoの機能を利用してプラットフォームごとにコードを切り替えることができます。

プラットフォームごとにコードを切り替える方法:

  1. Webのコードはxxx.tsx、ネイティブの場合はxxx.native.tsxと拡張子を変える
  2. 通常通りにインポートする
// component.native.tsx
export function Component() {
  return <NativeComponent>
    <Element1 />
    <Element2 />
  </NativeComponent>
}

// component.tsx
export function Component() {
  return <WebComponent>
    <Element1 />
    <Element2 />
  </WebComponent>
}

// 使用時
import { Component } from "./component"

function Parent() {
  return <Component />
}

Next.jsプロジェクトにExpoを導入する手順

Next.jsプロジェクトにExpoを導入する手順を簡単に紹介します。

  1. Next.jsプロジェクトの作成
  2. Expoの導入
  3. Tamaguiのセットアップ
  4. TamaguiとExpoをNext.jsと共存させる設定
  5. サンプルページの作成
  6. アプリの起動

1. Next.jsプロジェクトを作成

まず、TypeScriptを使用したNext.jsプロジェクト(pages router)を作成します。

npx create-next-app@latest universal-app-playground \
  --ts \
  --no-tailwind \
  --no-eslint \
  --no-app \
  --src-dir \
  --no-turbo \
  --no-import-alias \
  --empty

2. Expoの導入

次に、Expoと関連パッケージをインストールします。

# Expoの基本パッケージ
yarn add expo @expo/next-adapter

# Expo Router関連
npx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar

# その他の必要なパッケージ
npx expo install @expo/vector-icons @react-navigation/native expo-font expo-splash-screen expo-system-ui react react-dom react-native react-native-gesture-handler react-native-reanimated react-native-screens react-native-web

package.jsonを更新して、Expo Routerの設定と起動コマンドを追加します:

{
  "main": "expo-router/entry",
  "scripts": {
    "android": "expo start --android",
    "ios": "expo start --ios",
    ...
  }
}

app.jsonを作成し、Expo Routerを利用できるように設定を追加します:

{
  "expo": {
    "name": "My app",
    "slug": "my-app",
    "plugins": [
      [
        "expo-router",
        {
          "root": "src/expo"
        }
      ]
    ]
  }
}

"root": "src/expo"とすることによって、src/expo以下にネイティブでのページを記述することができます。

- src
  - expo
    - index.tsx
    - users
      - [id].tsx

3. Tamaguiのセットアップ

Tamaguiとその関連パッケージをインストールします:

yarn add tamagui @tamagui/config @tamagui/next-plugin webpack

tamagui.config.tsファイルを作成します:

import { config as configBase } from "@tamagui/config/v3";
import { createTamagui } from "tamagui";

const tamaguiConfig = createTamagui(configBase);

export default tamaguiConfig;

export type AppConfig = typeof tamaguiConfig;

declare module "tamagui" {
  interface TamaguiCustomConfig extends AppConfig {}
}

src/pages/_app.tsxを更新します:

import type { AppProps } from "next/app";
import { TamaguiProvider } from '@tamagui/core'
import config from '../../tamagui.config'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <TamaguiProvider config={config}>
      <Component {...pageProps} />
    </TamaguiProvider>
  )
}

src/pages/_document.tsxを作成または更新します:

import Document, { DocumentContext, Html, Head, Main, NextScript } from "next/document";
import tamaguiConfig from "../../tamagui.config";

export default class AppDocument extends Document {
  static async getInitialProps(ctx: DocumentContext) {
    const initialProps = await Document.getInitialProps(ctx);
    return {
      ...initialProps,
      styles: (
        <>
          {initialProps.styles}
          <style
            dangerouslySetInnerHTML={{
              __html: tamaguiConfig.getCSS(),
            }}
          />
        </>
      ),
    };
  }

  render() {
    return (
      <Html lang="en">
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

src/expo/_layout.tsxを作成します:

import { Stack } from "expo-router";
import { TamaguiProvider } from "tamagui";
import tamaguiConfig from "../../tamagui.config";

export default function RootLayout() {
  return (
    <TamaguiProvider config={tamaguiConfig}>
      <Stack>
        <Stack.Screen name="index" />
      </Stack>
    </TamaguiProvider>
  );
}

4. TamaguiとExpoをNext.jsと共存させる設定

next.config.mjsを更新します:

import { withExpo } from '@expo/next-adapter';
import { withTamagui as tamagui } from '@tamagui/next-plugin';

/** @type {import('next').NextConfig} */
const nextConfig = {
  // reactStrictMode: true,
  transpilePackages: [
    'react-native',
    'expo',
    // Add more React Native / Expo packages here...
  ]
};

const withTamagui = tamagui({
  config: "./tamagui.config.ts",
  components: ["tamagui"],
});

export default withExpo(withTamagui(nextConfig));

5. サンプルページの作成

src/pages/index.tsxを作成します:

import { Text, View } from "tamagui";

export default function HomeScreen() {
  return (
    <View>
      <Text>Hello from Next</Text>
    </View>
  );
}

src/expo/index.tsxを作成します:

import { Text, View } from "tamagui";

export default function HomeScreen() {
  return (
    <View>
      <Text>Hello from Expo</Text>
    </View>
  );
}

6. アプリの起動

Androidで起動する場合:

yarn android

Webで起動する場合:

yarn dev

これで、Next.jsプロジェクトにExpoを導入し、TamaguiでUIを共通化した基本的な開発環境が整いました。

まとめ

Next.jsとExpoを組み合わせ、Tamaguiを活用することで、Webとネイティブアプリの開発を効率的に進めることができます。この方法を使えば、Webとネイティブのコードの多くを共通化することができ、開発時間の短縮にもつながります。

Nextjsの恩恵も受けながら、ネイティブも同時並行で開発としたいという、そうな強欲野心的な願いを持っている方は、ぜひこのアプローチを試してみてください!

次回の記事では、既存のNext.jsのプロジェクトを段階的に移行する方法や、useRouterやLinkタグを利用したナビゲーションを共通化できるようにする方法を紹介したいと思います。

宣伝

休みの日のお出かけプランを自動的に作ってくれる komichi というアプリを開発しています!
ぜひ、使ってみてください! 意外と知らなかった近所のお店を発見できたり、楽しいですよ!
https://komichi.app/

Instagram、TikTokもやっているので、ぜひフォローをお願いします!
https://www.instagram.com/komichiapp?utm_source=ig_web_button_share_sheet&igsh=ZDNlZDc0MzIxNw==
https://www.tiktok.com/@komichiapp

参考文献

  1. Next.js 公式ドキュメント

  2. Expo 公式ドキュメント

  3. Tamagui 公式ドキュメント

  4. @expo/next-adapter

Discussion