「React Nativeって、Next.jsといっしょに使えないでしょ?」と思っているあなたへ。
対象読者
- 「RNってRNのコンポーネント使わないと行けないからNext.jsでは使えないでしょ?」と思っている人
- 一つのコードベースでWebもネイティブ(Android/iOS)も実現したい開発者
- WebではNext.js、ネイティブではExpoの恩恵を受けたいと考えている方
この記事を読んで分かること・できること
- 「あ、Flutter使わなくてもWebもネイティブも開発できるのね」
- 「React Native Web使わないで、Next.js使う方法もあるのね」
- Expo x Next.js の単純な構成のプロジェクトを作成します(コードも公開してます 🚀 )
利用する技術
技術 | 用途 |
---|---|
Next.js | Webアプリケーション開発 |
Expo | ネイティブアプリケーション開発 |
Tamagui | コンポーネントをWebとネイティブで共通化(これがすべて!) |
この構成のメリット
- ロジックだけでなく、UI実装も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 { 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 というアプリを開発しています!
ぜひ、使ってみてください! 意外と知らなかった近所のお店を発見できたり、楽しいですよ!
Instagram、TikTokもやっているので、ぜひフォローをお願いします!
参考文献
-
Next.js 公式ドキュメント
-
Expo 公式ドキュメント
-
Tamagui 公式ドキュメント
-
@expo/next-adapter
Discussion