🚀

「React Native(Expo)って、Next.jsといっしょに使えないでしょ?」と思っているあなたへ。

に公開

対象読者

  • WebではNext.js、ネイティブではExpoを使って開発したい方
  • さらに、Webとネイティブでコードを共通化したい方
  • 「React NativeってReact Nativeのコンポーネント使わないといけないからNext.jsでは使えないでしょ?」と思っている方

この記事を読んで分かること・できること

  • 「React Native Web使わないで、Next.js使う方法もあるのね」
  • 「Flutter使わなくてもWebもネイティブも開発できるのね」
  • Expo x Next.js の単純な構成のプロジェクトを作成します(コードも公開してます 🚀 )

https://github.com/zackerms/next-expo-universal-playground

自己紹介

zacker(ざっかー)といいます。
アプリ開発が大好きな大学院生です。

profile

「komichi」という2時間くらいの暇つぶしプランを作ってくれるサービスを作っています。
ぜひ、一度触ってみてください!
https://komichi.app/

利用する技術

技術 用途
Next.js Webアプリケーション開発
Expo ネイティブアプリケーション開発
Tamagui コンポーネントをWebとネイティブで共通化(これがすべて!)

この構成のメリット

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

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

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

課題点

  1. React NativeのUIコンポーネント(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の設定と起動コマンドを追加します:

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

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

app.json
{
  "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ファイルを作成します:

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を更新します:

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を作成または更新します:

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を作成します:

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を更新します:

next.config.mjs
import { withTamagui as tamagui } from '@tamagui/next-plugin';

/** @type {import('next').NextConfig} */
const nextConfig = {
};

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

export default withTamagui(nextConfig);

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

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

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を作成します:

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を共通化した基本的な開発環境が整いました。

まとめ

Tamaguiを活用することで、Webとネイティブアプリの開発を『同時に』進めることが可能になります!
ステキ!

参考文献

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

  2. Expo 公式ドキュメント

  3. Tamagui 公式ドキュメント

  4. @expo/next-adapter

Discussion