📷

React Nativeを使ってふわっちを作ってみた

2024/12/18に公開

みなさん、こんにちは。
jig.jp でアプリ開発を担当しているミヤケです。

今回は jig.jp Advent Calender 2024 の 18 日目として、 React Native を使ったアプリケーション開発の記事をお届けします。

作成したもの

React Native で弊社アプリのふわっちの一部をトレースしてみました。

React Native とは

React Native とは Meta によって開発されたフレームワークで、React をベースにして Android、iOS、Web などのマルチプラットフォームアプリケーションを開発することができるフレームワークです。React で書けるため React を使った経験がある Web エンジニアにとって学習コストが低く、参入しやすい という大きな強みがあります。React Native 製のアプリの代表的な事例として Facebook、Discord、Shopify、Amazon 系アプリ、Microsoft Office 系アプリが挙げられます。

似たようなマルチプラットフォームアプリケーション開発としてふわっちでも採用している Flutter があります。Google Trends を使って React Native と Flutter の比較(カテゴリを「オープンソース」としています)をしてみると、全体的に Flutter の方が検索されているものの期間によっては React Native の検索が多い箇所もあり React Native の需要も増えていることがわかります。

Expo とは

Expo とは Android、iOS、Web に対応したマルチプラットフォームアプリケーションを開発するためのオープンソースフレームワークです。React Native での開発を簡素化するフレームワークでもありプロジェクトのセットアップの簡易さや豊富なビルトイン機能がメリットとして挙げられます。React Native の公式から Expo を使った開発を勧められているので今回は Expo を使ってふわっち(一部)の開発をしました。

使用技術

Expo(フレームワーク)

Expo のプロジェクトを作るためには expo をターミナルからインストールする必要があります。公式を参考に expo をインストールしましょう

// npm
$ npm i expo

// yarn
$ yarn add expo

expo のインストールが完了すれば実際にプロジェクトを作成します。

$ npx create-expo-app my-app

今回 iOS のシミュレーターを使って開発します。Expo でシミュレーターでアプリを実行するには Expo Go を使います。この Expo Go を使うために Xcode Command Line Tools と Watchman をインストールしてください。

インストールが完了すれば Expo Go を使ってシミュレーター上でアプリを起動してみましょう。

$ npx expo start -c

下記のようなログが表示されるので i を入力して iOS のシミュレータ上に Expo Go をインストールしアプリを起動します。

...
› Press r │ reload app
› Press m │ toggle menu
› shift+m │ more tools
› Press o │ open project code in your editor

› Press ? │ show all commands

Logs for your project will appear below.

アプリが正常に起動できれば成功です 🎉

NativeWind(スタイリング)

React Native のスタイリングはデフォルトで StyleSheet.create メソッドを使っています。ですが Tailwind CSS が好きなのでそちらを使おうと思います。

Expo では Web のみ Tailwind CSS がサポートされており、ネイティブアプリでもスタイリングしたい場合は NativeWind を使う必要があります。

$ npx expo install nativewind tailwindcss react-native-reanimated react-native-safe-area-context

Tailwind CSS のセットアップをします。

$ npx tailwindcss init

tailwind.config.js が生成されるので下記コードをコピぺします。

/** @type {import('tailwindcss').Config} */
module.exports = {
  // NOTE: Update this to include the paths to all of your component files.
  content: ["./app/**/*.{js,jsx,ts,tsx}"],
  presets: [require("nativewind/preset")],
  theme: {
    extend: {},
  },
  plugins: [],
};

続いてルートに global.css を作成し下記コードをコピペします。

@tailwind base;
@tailwind components;
@tailwind utilities;

babel.config.js を作成し NativeWind を導入します。以下のコードをコピペします。

module.exports = function (api) {
  api.cache(true);
  return {
    presets: [
      ["babel-preset-expo", { jsxImportSource: "nativewind" }],
      "nativewind/babel",
    ],
  };
};

metro.config.js を作成し NativeWind を導入します。以下のコードをコピペします。

// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require("expo/metro-config");
const { withNativeWind } = require("nativewind/metro");

/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);

module.exports = withNativeWind(config, { input: "./global.css" });

最後に /app 直下の _layout.tsx に先ほど作成した global.css を import すると Tailwind CSS が使えるようになります。

// _layout.tsx
import "./global.css";

// index.tsx
import { View, Text } from "react-native";

export default function Page() {
  return (
    <View>
      {/* 赤い文字になる */}
      <Text className="text-red-500">赤い文字</Text>
    </View>
  );
}

Expo Router(ルーティング)

Expo Router は Expo および React Native においてファイルベースのルーティングを提供するライブラリです。ページごとにファイルを作成するので直感的なルーティングを構築することができ、Web 開発と同じようなルートの設定ができるため Next.js のようなファイルベースルーティングのフレームワークを使った方にとっては同じような感覚でルートの設定ができます。

/app
  ├── index.tsx          → "/" (ホームページ)
  ├── about.tsx          → "/about" (About ページ)
  ├── blog/
  │   ├── index.tsx      → "/blog" (ブログ一覧)
  │   └── [id].tsx       → "/blog/:id" (動的ルーティング)
// app/_layout.tsx
import { Stack } from "expo-router";
import "../global.css";

export default function RootLayout() {
  return <Stack />;
}

// app/index.tsx
import { Text, View } from "react-native";

export default function Page() {
  return (
    <View>
      <Text>あああ</Text>
    </View>
  );
}

続いて About ページを作成し遷移してみます。
ページの遷移には Link Component を使います。

// index.tsx
import { Link } from "expo-router";
import { Text, View } from "react-native";

export default function Page() {
  return (
    <View>
      <Text>あああ</Text>
      <Link href="/about">about</Link>
    </View>
  );
}

// about.tsx
import { Text, View } from "react-native";

export default function About() {
  return (
    <View>
      <Text>about</Text>
    </View>
  );
}

ヘッダー等のカスタマイズをページ単位で行いたい場合は <Stack.Screen> を使います。

import { Stack } from "expo-router";
import "../global.css";

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="index" options={() => ({ title: "ホーム画面" })} />
      <Stack.Screen name="about" options={() => ({ title: "About画面" })} />
    </Stack>
  );
}

Expo Image(画像表示)

Expo には画像表示ライブラリとして Expo Image があります。主な特徴として、

  • 高速な画像表示
  • 多様な画像フォーマットのサポート
  • 強力なキャッシュ機能
  • 画像切替のアニメーション

などが挙げられます。

Expo Image は下記コマンドでインストールできます。

$ npx expo install expo-image

Expo Image の使い方は下記の通りです。

<Image
  source="https://muchimuchi.dev/muchimuchi_logo_body.svg"
  style={{ width: 64, height: 64 }}
/>

ここで 1 つ欠点が。今回 Tailwind CSS を使ってスタイリングしていましたが、Expo Image は style props でしかスタイリングができませんでした(Tailwind CSS を使った方法があればぜひ教えていただきたいです...)

React Native Tab View(タブナビゲーション)

ふわっちのホーム画面のヘッダーにはカテゴリの TabView が表示されています。そこで React Native で TabView を実現する react-native-tab-view を採用します。以下のコードが実装例になります。

import { SceneMap, TabBar, TabView } from "react-native-tab-view";
import { useWindowDimensions } from "react-native";

export default function Page() {
  const layout = useWindowDimensions();
  const [index, setIndex] = useState<number>(0);

  // タブの設定
  // keyが識別子となり、titleがタブバーに表示されるラベル
  const routes = [
    { key: "category1", title: "カテゴリ1" },
    { key: "category2": title: "カテゴリ2" },
    { key: "category3": title: "カテゴリ3" },
    { key: "category4": title: "カテゴリ4" },
    // ...
    ];

  // タブごとのコンテンツ
  // keyに対しどのComponentを表示するかを決める
  // 例:category1のkeyでは Category1RouteのComponentを表示する
  const renderScene = SceneMap({
    category1: Category1Route,
    category2: Category2Route,
    category3: Category3Route,
    category4: Category4Route,
    category5: Category5Route,
    // ...
  });

  return (
    <TabView
     navigationState={{ index, routes }}
     renderScene={renderScene}
     onIndexChange={(index: number) => setIndex(index)}
     initialLayout={{ width: layout.width }}
     renderTabBar={( props ) => (
       <TabBar
         {...props}
         activeColor="#ff772a"
         inactiveColor="#4e4d4b"
         indicatorStyle={{ width: 100, height: 20 }}
         tabStyle={{ width: 100 }}
       />
     )}
    />
  )
}

所感

今回ふわっちの一部をトレースをしてみてですが、React のエコシステムがあるからこそ React Native や Expo の学習コストは低くすぐに馴染むことができました。特に Expo Router のファイルベースでのルーティングができることはかなり便利だと感じました。

とはいえ、Flutter には Riverpod と呼ばれる強力な状態管理ライブラリがあること、Google Trends を見ても現状 Flutter の方が検索数が多いことからまだまだ採用する企業は多いと思います。私個人としては React Native も Flutter もより進化し便利になって欲しいものです。

jig.jp Engineers' Blog

Discussion