React Native (Community CLI)

Mobile Bare Workflow Validation

Community CLI bare ワークフロー で動作する ReScript ベースの React Native アプリである — android/ の Gradle プロジェクト、ios/ の Xcode ワークスペース、Kotlin/Swift によるカスタムネイティブモジュール、あるいは Expo マネージドワークフローがまだサポートしていないツールチェーンへの直接アクセスが必要な場合に進む経路である。テンプレートには JavaScript / TypeScript / ReScript のサーフェスのみが含まれている; ネイティブプロジェクト自体は、ウィザード完了後に @react-native-community/cli を実行することで生成される。

ネイティブ側の要件 (ラップすべき Kotlin SDK、カスタム CocoaPod、環境ごとに分岐するビルドフレーバー) がある場合や、チームがすでに Android Studio / Xcode で暮らしている場合に本テンプレートを選ぶ。ニーズが Expo のマネージドパイプライン内に快適に収まる場合は、React Native (Expo) テンプレートのほうが日々のオーバーヘッドは少なく済む。

生成内容

my-project/
├── rescript.json                  # JSX + genType enabled, depends on @rescript/react + validation lib
├── package.json                   # react-native + community-cli + babel/metro configs
├── app.json                       # AppRegistry name + displayName
├── tsconfig.json                  # extends @react-native/typescript-config
├── index.js                       # RN entry — AppRegistry.registerComponent(appName, () => App)
├── App.tsx                        # one-liner: `import App from "./src/App.gen"; export default App;`
├── metro.config.js                # extends @react-native/metro-config + adds "mjs" to sourceExts
├── babel.config.js                # @react-native/babel-preset
├── src/
│   ├── App.res                    # @genType — useState + TextInput + Button + FlatList todo list
│   ├── ReactNative.res            # bindings for View / Text / TextInput / Button / FlatList / Style
│   ├── NativeGreeting.res         # example: NativeModules binding (bindings only — Kotlin/Swift impl by you)
│   ├── Validation.res             # zod or sury — parseDraftTodo for the new-todo input
│   └── __tests__/App.test.mjs     # vitest filesystem-only smoke test
├── README.md                      # CLI setup + Android Studio + Native Modules + Troubleshooting + Fallback
├── LICENSE                        # MIT, holder = project name
├── .nvmrc                         # Node 24
├── .gitignore                     # adds android/build/, ios/Pods/, *.hbc, *.keystore, *.apk, *.aab, *.ipa, …
├── .editorconfig                  # 2-space indent, LF line endings
└── .github/
    ├── dependabot.yml             # weekly npm updates
    └── workflows/ci.yml           # install + rescript build + vitest

箱に 入っていないもの: android/ios/ ディレクトリそのものである。これらは初回実行時に Community CLI によって生成される — 次のセクションを参照のこと。

ウィザードオプション

オプション

効果

Project name

npm nameapp.json の AppRegistry name + displayName、LICENSE のホルダー、および TODO リストの上に表示されるタイトル ({{projectName}} TODOs) になる

Package manager

npm / pnpm / yarn / bun。packageManager、README のインストール/実行スニペット、および CI キャッシュキーを設定する。README の "Community CLI Setup" セクションは、選択されたインストール + 実行コマンド ({{installCmd}} / {{execReactNative}}) を置換する

Validation library

zodsury。同梱する src/Validation.res のバリアントと、dependencies に追加される依存を選択する

主要な依存

パッケージ

用途

バージョン

rescript

ReScript コンパイラ

TemplateVersions.RESCRIPT

@rescript/core

標準ライブラリ

TemplateVersions.RESCRIPT_CORE

@rescript/runtime

コンパイル済み .res.mjs がインポートするランタイムスタブ

TemplateVersions.RESCRIPT_RUNTIME

@rescript/react

React バインディング (JSX 有効化された rescript.json)

TemplateVersions.RESCRIPT_REACT

react

React Native と共有する UI ライブラリ

TemplateVersions.REACT

react-native

ネイティブ UI プリミティブ

TemplateVersions.REACT_NATIVE

zod または sury

バリデーションバックエンド (フォーム入力パーサー)

TemplateVersions.ZOD / SURY

@react-native-community/cli (dev)

android/ios/ をスキャフォールドし、Metro を実行し、ネイティブビルドをトリガーする CLI

TemplateVersions.RN_COMMUNITY_CLI

@react-native/babel-preset (dev)

RN バンドラーが期待する Babel プリセット

TemplateVersions.RN_BABEL_PRESET

@react-native/metro-config (dev)

metro.config.js で拡張するデフォルトの Metro 設定

TemplateVersions.RN_METRO_CONFIG

@react-native/typescript-config (dev)

App.tsx + .gen.tsx ファイル用の TS 設定ベース

TemplateVersions.RN_TYPESCRIPT_CONFIG

vitest (dev)

スモークテストランナー — pure-Node で、RN は決してロードしない

TemplateVersions.VITEST

@vitest/coverage-v8 (dev)

test:coverage のカバレッジプロバイダ

TemplateVersions.VITEST_COVERAGE_V8

typescript (dev)

App.tsx および genType が出力する .gen.tsx ファイルに必要

TemplateVersions.TYPESCRIPT

@types/react (dev)

genType が出力対象とする React サーフェスの型定義

TemplateVersions.REACT_TYPES

ネイティブプロジェクトの生成

ウィザードは @react-native-community/cli を代わりに実行することはない。プロジェクトが生成され依存がインストールされた後、次を実行する:

<your install command>
npx @react-native-community/cli init-android
npx @react-native-community/cli init-ios        # macOS only

使用している CLI のリビジョンで init-android / init-ios が利用できない場合は、フォールバックとして上流テンプレートを使うこと:

npx @react-native-community/cli@latest init tmp
# then copy tmp/android (and tmp/ios) into this project

前提条件:

ターゲット

ツールチェーン

Android

JDK 21, Android Studio, Android SDK + NDK, watchman (macOS/Linux)

iOS

macOS, Xcode 16+, CocoaPods

README にはこれらの手順を拡張したバージョンが同梱されており、コマンドラインに選択したパッケージマネージャを置換する。

主要なファイル

App.tsx

意図的に簡素である — リソースファイルからロードされるのではなく、ジェネレータ内のインライン文字列である:

import App from "./src/App.gen";

export default App;

AppRegistry (index.js 内) は () => App を呼び出し、ファクトリは呼び出さない。genType はコンパイル時に src/App.res の隣に対応する App.gen.tsx を出力する。

index.js

ネイティブ側のエントリポイント。標準的な React Native のブートストラップである:

import { AppRegistry } from "react-native";
import App from "./App";
import { name as appName } from "./app.json";

AppRegistry.registerComponent(appName, () => App);

metro.config.js

バンドラー設定で唯一デフォルトと異なる箇所: Metro の sourceExts"mjs" を追加して、ReScript のコンパイル出力を拾えるようにしている。

const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");

const defaultConfig = getDefaultConfig(__dirname);

const config = {
  resolver: {
    sourceExts: [...defaultConfig.resolver.sourceExts, "mjs"],
  },
};

module.exports = mergeConfig(defaultConfig, config);

.res.mjs ファイルを指す Module not found エラーが発生した場合、まず確認すべきはこのリゾルバエントリである。

babel.config.js

1 行の設定:

module.exports = { presets: ["module:@react-native/babel-preset"] };

ReScript は .res を標準準拠の ES モジュールにコンパイルする — ReScript 出力の Babel 変換は不要である; このプリセットは JS / TS 層のみに供される。

src/App.res

対話的な TODO リスト。Expo テンプレートのデモと同一である — todosdrafterror 用の useState スライス、追加前のバリデーションゲート、レンダリング用の FlatList — そして同じ @genType @react.component アノテーションを使用するため、genType が App.gen.tsx を出力する。

let addTodo = () => {
  switch Validation.parseDraftTodo(draft) {
  | Error(message) => setError(_ => Some(message))
  | Ok({text}) =>
    let nextId = todos->Array.reduce(0, (max, t) => t.id > max ? t.id : max) + 1
    setTodos(prev => prev->Array.concat([{id: nextId, text}]))
    setDraft(_ => "")
    setError(_ => None)
  }
}

src/ReactNative.res

Expo テンプレートと同じ最小限のバインディングセット: ViewTextTextInputButtonFlatList に加えて Style.make ヘルパー。各モジュールは @module("react-native") @react.component external make: (...) => React.element = "..." パターンに従う; コピーしてさらにコンポーネントをバインドするか、コミュニティモジュールについては "react-native" の代わりにサードパーティのパッケージ名を使う。

src/NativeGreeting.res

多くのチームが bare ワークフローを選ぶ理由: カスタム Kotlin/Swift NativeModule をバインドする実例である。

module NativeGreeting = {
  @module("react-native") @scope("NativeModules")
  external module_: {"greet": string => promise<string>} = "NativeGreeting"

  let greet = (name: string): promise<string> => module_["greet"](name)
}

このファイルには バインディングのみ が含まれる。Kotlin (Android) または Swift (iOS) 実装は、android/app/src/main/java/... または ios/ 配下に別途追加する必要がある。テンプレートは意図的にネイティブ側を 同梱しない — プラットフォームと最小 OS ターゲットごとに大きく異なるためであり、古い例を同梱することは助けになるよりも害となる。公式の RN ガイドに従うこと:

src/Validation.res

parseDraftTodo: string => result<draftTodo, string>。Expo テンプレートと同じ形と振る舞いである — 入力をトリムし、空のエントリと 120 文字を超えるエントリを拒否し、それ以外の場合は draftTodo レコードを返す。

src/__tests__/App.test.mjs

ファイルシステムのみのスモークテスト — コンパイル成果物が存在するかを existsSync で確認する。App.res.mjs を素の Node からインポートすると react-native を引き込んでしまい、Vitest はこれを実行できない。これはビルドのカナリアとして扱うこと; 振る舞いのテストには Jest + jest-react-native を使う。

tsconfig.json

@react-native/typescript-config"strict": true"noImplicitAny": true で拡張する。この strict のペアが genType の出力する .gen.tsx ファイルを厳格に保つ。

.gitignore

bare ワークフローでは Expo フレーバーよりも積極的な ignore が必要となる: ビルド出力 (android/build/android/app/build/ios/build/ios/Pods/)、gradle キャッシュ (android/.gradle/)、ローカル Android 設定 (android/local.properties)、Hermes バイトコード (*.hbc)、署名素材 (*.keystoreios/.xcode.env.local)、ビルド成果物 (*.apk*.aab*.ipa)、および TypeScript 増分ビルド情報 (*.tsbuildinfo) である。

android/ios/ 自体は ignore されない ことに注意 — 生成後は設定 (build.gradlePodfileInfo.plist など) をコミットして、チームが再現可能なビルドを行えるようにする。

npm スクリプト

スクリプト

説明

start

react-native start — Metro を起動する

android

react-native run-android — 接続されたエミュレータ/デバイス上で Gradle ビルド + インストール

ios

react-native run-ios — シミュレータ/デバイス上で Xcode ビルド + インストール (macOS のみ)

test

vitest run — ソースのスモークテストを実行する

test:coverage

vitest run --coverage — 同上、v8 カバレッジ付き

res:build

rescript — ワンショットの ReScript コンパイル

res:dev

rescript -w — 保存時に再コンパイル

res:clean

rescript clean — 生成された .res.mjs.gen.tsx を削除

典型的なセッションでは 3 つのターミナルを使う: npm run res:dev (ReScript ウォッチャー)、npm start (Metro)、および npm run android / npm run ios (もしくはネイティブビルド用に Android Studio / Xcode) である。

2日目以降のレシピ

プロジェクトを開いた後の ReScript 側のエディタワークフローについては 機能概要 を参照のこと。

補足

  • ネイティブプロジェクトは別途生成する。 ウィザードには JS/TS + ReScript のサーフェスしか含まれていない。ウィザードの後で npx @react-native-community/cli init-android / init-ios を実行するか、上流テンプレートのフォールバックを使うこと — 上記の "ネイティブプロジェクトの生成" を参照のこと。

  • Android Studio ではプロジェクトルートではなく android/ を開く。 Gradle が IDE のルートであることを期待する。File > Settings > Build, Execution, Deployment > Build Tools > Gradle で Gradle JDK を 21 に設定すること。Run > Run 'app' を押す前に Metro が起動していなければならない。

  • metro.config.js がデフォルトと異なる唯一のファイルである。 mjs の sourceExt によって ReScript 出力が解決可能になる。mergeConfig でさらにカスタマイズを加える場合、そのリゾルバエントリを保持しないと .res.mjs のインポートが "Module not found" で失敗し始める。

  • ネイティブモジュールはバインディングのみである。 NativeGreeting.res は ReScript 側を示す; Kotlin / Swift 側は自分で書く必要がある。テンプレートはどの RN ネイティブモジュールアーキテクチャ (legacy bridge vs. TurboModules / Fabric) を対象とするかについては中立を保つ — ファイル内のリンクは明確さのために legacy ドキュメントを指している。

  • @react-native-community/cli は速いペースで動く。 コマンド名やフラグはマイナーリリース間で漂流する。README のスニペットが現在の CLI から乖離した場合は、https://reactnative.dev の公式ドキュメントにフォールバックすること; テンプレートの ReScript 側 (rescript.jsonsrc/*.resmetro.config.js) は影響を受けない。

  • よくある落とし穴 (README の Troubleshooting セクションにも記載されている):

    • Metro がデバイスに到達できない: adb reverse tcp:8081 tcp:8081

    • 古い Metro キャッシュ: --reset-cache を付けて Metro を再起動する

    • 依存アップグレード後の Gradle エラー: cd android && ./gradlew clean の後に再ビルドする

    • .res.mjs の Module not found: metro.config.jsresolver.sourceExtsmjs を追加し続けているか確認する

  • スモークテストは意図的に無力である。 Expo フレーバーと同じ制約 — App.res.mjs は素の Node からインポートできない。コンポーネントレベルのテストには適切な RN プリセット付きの Jest を使うこと。

  • genType は必須である。 起動経路 index.js App.tsx src/App.gen が動作するのは、rescript.json で genType が有効化されているからこそである。これを無効化すると App.tsx が直ちに壊れる。