Next.jsとExpoを組み合わせた最強のクロスプラットフォーム開発環境
対象読者
- 一つのコードベースでWebもネイティブも実現したい開発者
- WebではNext.js、ネイティブではExpoの恩恵を受けたいと考えている方
利用する技術
技術 | 用途 |
---|---|
Next.js | Webアプリケーション開発 |
Expo | ネイティブアプリケーション開発 |
Tamagui | コンポーネントをWebとネイティブで共通化 |
Solito | ナビゲーション(ページ遷移)をWebとネイティブで共通化(次回の記事で使い方を書きます) |
この構成のメリット
- ロジックだけでなく、ViewもWebとNativeで共通化できる
- 完全に共通化することも、プラットフォーム固有の機能を利用する場合はその部分だけを分けることも可能
- Next.jsとExpoで同様の方法でルーティングが可能(Pages Router、Expo Router)
React NativeコンポーネントとNext.jsの課題
React NativeはWebとネイティブアプリの開発を同じ言語で行えるという利点で注目されてきました。
しかし、Viewの完全な共通化には課題がありました。
課題点
- React Nativeコンポーネント(
react-native
パッケージ)はNext.jsのビルドに対応していない - Next.jsのSSR、SSGなどの高度な機能が使えない
この課題を解決するのが、Tamaguiです。
Tamagui
Tamaguiは、「ロジックだけでなく、Viewも共通化しつつ、Next.jsの恩恵も受けたい」という開発者の強欲を叶えてくれるフレームワークです。
Tamaguiの特徴
- ビルド時にプラットフォームごとに適切なコードに変換
- Webの場合は
div
タグとCSS、Nativeの場合はreact-native
のView
コンポーネントを出力 - 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の機能を利用してプラットフォームごとにコードを切り替えることができます。
プラットフォームごとにコードを切り替える方法:
- Webのコードは
xxx.tsx
、ネイティブの場合はxxx.native.tsx
と拡張子を変える - 通常通りにインポートする
// 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を導入する手順を簡単に紹介します。
- Next.jsプロジェクトの作成
- Expoの導入
- Tamaguiのセットアップ
- TamaguiとExpoをNext.jsと共存させる設定
- サンプルページの作成
- アプリの起動
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 というアプリを開発しています!
ぜひ、使ってみてください! 意外と知らなかった近所のお店を発見できたり、楽しいですよ!
Instagram、TikTokもやっているので、ぜひフォローをお願いします!
参考文献
-
Next.js 公式ドキュメント
-
Expo 公式ドキュメント
-
Tamagui 公式ドキュメント
-
@expo/next-adapter
Discussion