🚀

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

2024/09/17に公開

対象読者

  • 「RNってRNのコンポーネント使わないと行けないからNext.jsでは使えないでしょ?」と思っている人
  • 一つのコードベースでWebもネイティブ(Android/iOS)も実現したい開発者
  • WebではNext.js、ネイティブではExpoの恩恵を受けたいと考えている方

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

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

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

利用する技術

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

この構成のメリット

  1. ロジックだけでなく、UI実装も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 { 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を作成します:

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

まとめ

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

宣伝

休みの日のお出かけプランを自動的に作ってくれる 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