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 |
Package manager |
npm / pnpm / yarn / bun。 |
Validation library |
|
主要な依存¶
パッケージ |
用途 |
バージョン |
|---|---|---|
|
ReScript コンパイラ |
|
|
標準ライブラリ |
|
|
コンパイル済み |
|
|
React バインディング (JSX 有効化された |
|
|
React Native と共有する UI ライブラリ |
|
|
ネイティブ UI プリミティブ |
|
|
バリデーションバックエンド (フォーム入力パーサー) |
|
|
|
|
|
RN バンドラーが期待する Babel プリセット |
|
|
|
|
|
|
|
|
スモークテストランナー — pure-Node で、RN は決してロードしない |
|
|
|
|
|
|
|
|
genType が出力対象とする React サーフェスの型定義 |
|
ネイティブプロジェクトの生成¶
ウィザードは @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 テンプレートのデモと同一である — todos、draft、error 用の 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 テンプレートと同じ最小限のバインディングセット: View、Text、TextInput、Button、FlatList に加えて 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)、署名素材 (*.keystore、ios/.xcode.env.local)、ビルド成果物 (*.apk、*.aab、*.ipa)、および TypeScript 増分ビルド情報 (*.tsbuildinfo) である。
android/ と ios/ 自体は ignore されない ことに注意 — 生成後は設定 (build.gradle、Podfile、Info.plist など) をコミットして、チームが再現可能なビルドを行えるようにする。
npm スクリプト¶
スクリプト |
説明 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
典型的なセッションでは 3 つのターミナルを使う: npm run res:dev (ReScript ウォッチャー)、npm start (Metro)、および npm run android / npm run ios (もしくはネイティブビルド用に Android Studio / Xcode) である。
2日目以降のレシピ¶
React コンポーネントの作成 — 同じ JSX パターンが適用される (
<div>をViewに置き換える)Import の最適化 — バインディングが増えるにつれて
src/ReactNative.resとsrc/NativeGreeting.resを簡潔に保つデッドコードの検出 — 画面 + ネイティブモジュールのサーフェスが拡大するにつれて有用になる
プロジェクトを開いた後の 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.json、src/*.res、metro.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.jsがresolver.sourceExtsにmjsを追加し続けているか確認する
スモークテストは意図的に無力である。 Expo フレーバーと同じ制約 —
App.res.mjsは素の Node からインポートできない。コンポーネントレベルのテストには適切な RN プリセット付きの Jest を使うこと。genType は必須である。 起動経路
index.js → App.tsx → src/App.genが動作するのは、rescript.jsonで genType が有効化されているからこそである。これを無効化するとApp.tsxが直ちに壊れる。