既存のウェブReactアプリをExpoを使ってネイティブなiOS/Androidアプリに移行させるガイドです。 次のような場合に使用: - ウェブサイトをモバイルアプリに変更したい場合 - Next.js・Vite・CRAなどで構築したReactコードベースをReact Nativeに移植したい場合 - ウェブのコードをネイティブアプリ側で段階的に再利用したい場合 - ウェブの開発手法(DOM、CSS、React Router、localStorage、windowなど)がネイティブアプリでどのように対応するかを知りたい場合 このスキルは、ウェブからネイティブへの移行全体をカバーする総合ガイドです。DOMコンポーネント(HTML要素の仕組み)そのものについては、`use-dom`スキルをご使用ください。
Migrate an existing web React app to a native iOS/Android app with Expo. Use when the user wants to turn a website into a mobile app, port a Next.js/Vite/CRA React codebase to React Native, reuse web code on native incrementally, or asks how web idioms (the DOM, CSS, React Router, localStorage, window) map to native. This is the end-to-end migration guide; use the `use-dom` skill for the DOM-component mechanism itself.
Webで作ったReactアプリをネイティブアプリに変換することはできません。トランスパイラ(別の形式への自動変換ツール)も存在しません。代わりに、段階的に移行します。絞め殺す無花果(いちじく)が木を包み込んで徐々に置き換わるように、ネイティブの殻を用意して、1日目は全体のWeb UIをそこで動かし、その後、優先度の高い順にひとつひとつの画面をネイティブに置き換えていきます。このスキルはその作業の流れをまとめたもの。各ステップは既存のExpoスキルに渡すため、同じ説明を繰り返しません。Expoの公式ガイド「From Web to Native with React」の実践的な形です。背景は公式ガイドをご覧ください。
flowchart TD
A1[1 · 現状把握: やることリストを書く] --> A2[2 · Expoの殻を組み立てる]
A2 --> A3[3 · DOM部品の殻<br/>· use-dom · 初日にリリース]
A3 --> A4[4 · 画面をネイティブに置き換える<br/>重要度の高い順 · building-native-ui]
A4 -->|さらに画面がある| A4
A4 --> A5[5 · データ・認証・保存を接続<br/>· native-data-fetching]
A5 --> A6[6 · リリース · expo-deployment]
全面的に書き直さず、段階的に移行する。 一気にやるのではなく、各ステップを終えるたびにアプリが動作する状態を保つ。
初日にリリースできる状態まで進める。 Web UIはDOM部品の殻(ステップ3)の中で動き始めます。何もネイティブ化する前です。ここがマイルストーン。その後はすべて改善です。
価値の高い部分だけネイティブ化する。 ユーザーがよく使う画面をネイティブにしたら、残りはWebビューのままでいい。DOM画面は1つ約2MBのWeb実行環境を持つので、すべてをDOMで作る理由はありません。
ネイティブ化とは、見た目を変えることではなく、設計し直すこと。 ネイティブ化した画面は、AppleやGoogleが作ったように見えるべき。Webページをテーマを変えるだけでは駄目です。まず@expo/uiを使ってください。本物のSwiftUIやCompose(Androidネイティブ)をレンダリング(描画)するので、OSそのものに見えます。React Native(ネイティブコンポーネント)の基本部品は、カスタムレイアウトが必要な場合だけの代替案です。加えて、プラットフォーム型ナビゲーション(expo-routerのネイティブタブ、大きいタイトル)、液状ガラス、@expo/uiほかのネイティブ部品、モバイル向けの動き(ボトムシート、スワイプ、振動)など。Web→ネイティブのパターンマッピングは./references/native-patterns.mdにあります。まだウェブサイトに見えたら、作り直しではなく移植しただけです。
コンパイル結果で判断するのではなく、実際に動かして確認する。 クリーンなビルドが成功しても、実装が正しいわけではありません。空のWebビューだってコンパイルできます。実際に画面を動かし、ピクセル単位ではなく、コンテンツと動作でWeb版と比べます。ネイティブ化した画面は、よりネイティブに見えるべき。同じに見える必要はありません。
各ステップの工程を活かし、一から作り直さない。 既存スキルへのルート案内が各ステップの役割です。ここでの価値は、進める順番と落とし穴の説明。言い回しごとの対応は./references/false-friends.mdにあります。
移行は何度も繰り返すループなので、最初の一手はゴール目標を書いて動かすこと。1画面ずつ手で処理するのではなく。./references/run-as-goal.mdでこのアプリ向けの目標を記入して実行してください。毎回の繰り返しでこのスキルを読み直すので、/goalを実行するたびに手引き書とやることリストが再読み込みされ、次の画面が進みます。自動で現状把握ステップも立ち上がります。それから/goalで実行してください。ループ機能がない場合は、migration-goal.mdに書いて、ユーザーに立ち上げさせてください。以下のステップは、各繰り返しで何をするかです。ループがなければ、手で実行してください。
既存のコードベースがない、Web開発者として新規にネイティブアプリを作る? 以下のステップは不要です。
building-native-uiを使い、./references/false-friends.mdを見ながらWeb→ネイティブの言い回しマップを確認してください。以下は既存Webアプリがあることを前提としています。
リポジトリを読んで、migration-progress.mdを作る。これが以降の移行で確認していく、変わらないやることリストです。2つの切り口で仕分けます。
画面とバックエンド(サーバー側)を分ける。 ページルート(page.tsx)は移行する画面です。サーバールート(route.ts)、データベース層、認証処理はサーバー側に残します。バックエンドをどうするか決めます。今のデプロイ場所に置いたままにするか、EAS Hosting(Expoのホスティングサービス)に移すか(expo-api-routesを使用)。
各画面を分類する。どう移行すべきか:そのまま移植する(表示するだけ → Webビューとして動かしたまま)、すぐにネイティブ化する(よく使う、またはネイティブの動き・操作感が必要:ジェスチャー、リスト、キーボード)、あとで移植する、ハイブリッド(ネイティブの殻の中にWeb部分を入れる、例:チャットリストの中にマークダウンレンダラー)。
読みながら、フレームワークの特徴を記す。RSC(サーバーコンポーネント)か、それともクライアントか、Tailwind/shadcnの使い方、どこでデータを取ってくるか。これらが各画面の移行方法を決めます。false-friendsにマッピングがあります。特に、非同期サーバーコンポーネント(async Server Components)は、クライアント側のデータ取得と、表示用コンポーネント分けてから移行します。外部サービス・SDK も記す。 ブラウザ用SDKは移行できません。false-friendsの「Services & SDKs」を見てください。とりわけ支払い機能は置き換えではなく、作り直しです。ネイティブアプリ内の有料コンテンツはアプリストアのIAP(アプリ内購入)を使う必要があります。RevenueCat経由で、~30%が手数料です。Stripeではなく。ビジネスモデルの判断は今のうちにします。App Storeの審査の段階では遅いです。すべてのルートが仕分けられ、すべての画面が分類されるまで、やることリストは信頼できません。
create-expo-appで新規作成してから、Expo Routerでウェブアプリのルートを写す。Next.jsのツリー構造ほぼそのまま移植できます。[id]/page.tsx → [id].tsxに、ルートはsrc/app/に置く場合があります。空の画面を、ルート分だけ作ります。
すべての画面をDOM部品として移す('use dom'、use-domスキルを参照)。ネイティブルートが各画面を表示します。アプリ全体がスマートフォンで動き始めます。ネイティブ化はまだ何もしていません。1画面ずつ編集が必要です。サーバーコンポーネントをほぐして、フレームワークのインポートを入れ替えて(next/linkなど)、スタイルを持ってくる。すべてfalse-friendsで説明しています。実際に動かして確認します。下の「動かして確認する」を見てください。この状態で、TestFlightにリリース可能です。
migration-progress.mdを上から下へ進める。各画面に対して、ネイティブ版として設計し直す。ウェブレイアウトを移さない。@expo/ui を最初に検討してください。本物のSwiftUI/Compose(Androidネイティブ)。ボタン、リスト、ボトムシート、ピッカー、スライダー。どのウェブパターンがどのネイティブ部品に対応するかは./references/native-patterns.mdで。その次、プラットフォーム型ナビゲーション(expo-routerのネイティブタブ、大きいタイトル)とモバイル向けの動き(スワイプ、振動、慣性/反転スクロール)。React Native基本部品は、カスタムレイアウトが必須の時だけ。各言い回しはfalse-friendsで。@expo/uiとDOM部品の両方がExpo Go(SDK 56以降)で動きます。カスタムネイティブモジュールが必要な時だけ、開発用ビルド(expo-dev-clientスキルを参照)を作ります。コンテンツと動作をウェブ版と比べます。下の「動かして確認する」参照。見た目はより本物のネイティブに見えるべき。確認したら、チェックを入れる。1画面ずつ、アプリは常に動く状態。耐久的なやることリストを繰り返すループなので、無人で動かせます。ゴールループに渡してください(./references/run-as-goal.md)。
ウェブのデータ層はそのまま移行できません。相対パスのURL取得、クッキーセッション、localStorage、環境変数。すべて変わります。false-friendsで置き換え方が書いてあります。native-data-fetchingでリクエストとキャッシュ。バックエンドをEAS Hostingに移した場合はexpo-api-routesを足します。
expo-deploymentでストアビルド(App Store / Play Store / TestFlight)。その後、OTA配信(ユーザーのアプリを更新サーバーから自動更新)はEAS Updateを使います。
expo exportが緑に変わっても、画面が描画されるのが確実とは限りません。バンドル(一つのファイル)はできるけど、表示が空白か崩れている場合もあります。殻を作った後、なおかつネイティブ化した画面の後、毎回、2つのアプリを動かして同じルートで比べます。
agent-browser**で確認します(Vercel LabsのCLI):ルートをopenして、アクセシビリティツリー(画面構造)をsnapshot --json、スクリーンショット。describe / debugger-component-treeで構造、flowで毎回確認を回す。コンテンツと動作で一致を判定します。ピクセルではなく。ネイティブ化した画面は、よりネイティブに見えるべき。同じに見える必要はありません。(DOM殻の段階は例外。ウェブUIそのものなので、合わせるべき)。動きの感覚はネイティブの一部で、スクリーンショットでは捉えられません。アニメーションやジェスチャーのある画面は、短い動画を撮影してください。スティル画像だけじゃなく(native-patterns.mdの「Feel」参照)。このループは、使うツールにこだわります。agent-browserまたはargentがインストール済みじゃなければ、ユーザーに聞いて、進める前にインストールしてください。手動スクリーンショットに逃げないでください。全レシピとセットアップは[`./
A web React app does not convert to native — there is no transpiler. It migrates, screen by screen, the way a strangler fig grows around a tree and slowly replaces it: stand up a native shell, run the whole web UI inside it on day one, then strangle each screen into native in priority order. This skill is the spine that orders the work; each step hands off to an existing Expo skill rather than re-explaining it. It operationalizes Expo's From Web to Native with React — read that for the why.
flowchart TD
A1[1 · Assess: write the worklist] --> A2[2 · Scaffold Expo shell]
A2 --> A3[3 · DOM-component shell<br/>· use-dom · SHIP DAY ONE]
A3 --> A4[4 · Strangle screens to native<br/>highest-value first · building-native-ui]
A4 -->|more screens| A4
A4 --> A5[5 · Wire data / auth / storage<br/>· native-data-fetching]
A5 --> A6[6 · Ship · expo-deployment]
@expo/ui first — it renders real SwiftUI/Compose, so it feels exactly like the OS; styled RN primitives are the fallback for custom layouts only. Plus platform navigation (expo-router: NativeTabs, large titles), liquid glass and native components via @expo/ui, and mobile UX (sheets, swipe, haptics). The web→native pattern map is ./references/native-patterns.md. If it still feels like a website, you ported instead of redesigned../references/false-friends.md.The migration is a long repeat-until-done loop, so the first move is to write the goal objective and launch it — not to grind screens by hand. Fill the objective in ./references/run-as-goal.md for this app and present it; it re-reads this skill every iteration, so each /goal turn reloads the playbook + worklist and drives the next screen (it even self-bootstraps the assess step). Then run /goal with it — or, if the harness can't loop, write it to migration-goal.md and have the user launch it. The steps below are what each iteration does; run them by hand only if you're not looping.
No repo to migrate — just building native fresh as a web dev? You don't need these steps: use
building-native-ui, and keep./references/false-friends.mdopen for the web→native idiom map. Everything below assumes an existing web app.
Read the repo and produce migration-progress.md, the durable worklist the rest of the migration checks off. Make two cuts:
page.tsx) are screens you migrate; server routes (route.ts), the ORM, and auth handlers stay server-side. Decide the backend once: keep it deployed (the native app becomes an HTTP client) or move it to EAS Hosting (expo-api-routes).Note the framework signals as you read — RSC vs client, Tailwind/shadcn, where data is fetched — since they decide how each screen ports (false-friends has the mappings; async Server Components in particular must be split into a client fetch + a presentational component before they can move). Flag third-party services/SDKs too — browser SDKs don't carry over (false-friends → Services & SDKs); payments especially is a fork, not a swap (in-app digital goods must use store IAP via RevenueCat, ~30% — not Stripe), a business-model call to make now, not at App Store review. The worklist is only trustworthy once every route is sorted and every screen bucketed.
create-expo-app, then mirror the web routes in Expo Router — Next's tree maps almost 1:1 (note [id]/page.tsx → [id].tsx, and routes may live in src/app/). Empty screens, one per route.
Bring every screen over as a DOM component ('use dom', per the use-dom skill) rendered by its native route, so the whole app runs on a phone before anything is nativized. Expect per-screen edits — unwrapping Server Components, swapping framework imports (next/link), carrying the styling over — all covered in false-friends. Then verify by running (below); this is shippable to TestFlight as-is.
Walk migration-progress.md top-down. For each screen, redesign it native — don't port the web layout. Reach for @expo/ui first (real SwiftUI/Compose — buttons, lists, sheets, pickers, sliders; ./references/native-patterns.md maps which web pattern becomes which native component), then platform navigation (expo-router — NativeTabs, large titles) and mobile UX (swipe, haptics, momentum/inverted scroll); RN primitives only for custom layouts. Consult ./references/false-friends.md for each idiom. @expo/ui and DOM components both run in Expo Go (SDK 56+) — a dev build (the expo-dev-client skill) is only needed for custom native modules. Verify content and behavior against the running web original (the look should become more native), then check it off. One screen per pass, app shippable throughout. It's a loop over a durable worklist, so it can run unattended — hand it to a goal loop (./references/run-as-goal.md).
The web data layer doesn't survive the move — relative fetches, cookie sessions, localStorage, and env vars all change (swaps in false-friends). Use native-data-fetching for requests and caching; add expo-api-routes if the backend moved to EAS Hosting.
expo-deployment for the store builds (App Store / Play / TestFlight), EAS Update for OTA pushes after.
A green expo export proves a screen bundles, not that it renders — a screen can build and still render blank or mis-render. So after the shell and after every nativized screen, compare the two running apps for the same route:
agent-browser (vercel-labs CLI): open the route, snapshot --json the accessibility tree, screenshot.argent: describe / debugger-component-tree for structure, flow to replay the check each pass.Pass on parity of content and behavior — not pixels: a nativized screen should look more native than the web, never identical (the DOM-shell stage is the exception — there it is the web UI, so it should match). Feel is part of native and can't be screenshotted — for screens with transitions or gestures, capture a short recording, not just a still (see native-patterns.md → Feel). This loop is opinionated about its tooling: if agent-browser or argent isn't installed, ask the user and install it before proceeding — don't fall back to manual screenshots. Full recipe and setup in ./references/verify-on-device.md.
./references/false-friends.md — web idiom → native equivalent + the gotcha for each. The lookup for steps 3–5, and for any web dev unlearning idioms../references/native-patterns.md — web UX pattern → native redesign (@expo/ui-first). The step-4 redesign playbook so screens feel OS-native, not reskinned../references/verify-on-device.md — the two-agent parity recipe: drive the web app (browser agent) and the native app (argent), open the same route, compare../references/run-as-goal.md — a ready-shaped, migration-specific goal objective for driving step 4 unattended (re-reads this skill each iteration).原文・著作権は Anthropic および各プラグイン作者に帰属します。日本語訳は Claude API による自動翻訳です。