Claude Code and React Native: Practical Expo, Native, and Release Guide
Use Claude Code with React Native: Expo vs bare, permissions, Metro errors, emulator tests, accessibility, release.
Decide the mobile boundary before asking Claude Code
Claude Code is useful in React Native because it can do more than generate one screen. It can read the project, edit TypeScript, run validation commands, explain native rebuild risk, and hand back a verification note. That matters because mobile work crosses more boundaries than a normal web component: iOS permissions, Android package names, Metro resolution, development builds, emulator behavior, accessibility, and release checks.
The weak request is “build a React Native app.” The strong request says whether this is an Expo app, an Expo development build, or a bare React Native app with native folders owned by the team. It also says which files are safe to edit, which commands require approval, and which device must be tested.
The official baseline I use is Claude Code overview, Claude Code permissions, Expo documentation, Expo development builds, React Native environment setup, React Native Native Modules, Metro troubleshooting, and React Native accessibility. If Claude Code itself is new to your team, start with the Claude Code getting started guide and then tighten the workflow with the permissions guide.
Start with a project map
A project map is a short operating brief for the agent. Think of it as the harness, meaning the working frame that tells Claude Code where it can move and how success is checked. Without this, Claude Code can still produce working code, but it may miss native rebuilds, platform gaps, or release constraints.
flowchart LR
A["Project map"] --> B["Claude Code task"]
B --> C["JS/TS implementation"]
B --> D["Native config"]
C --> E["Metro and unit tests"]
D --> F["Dev build / emulator"]
E --> G["Accessibility check"]
F --> G
G --> H["Release checklist"]
Use a small file like this before asking for implementation:
# React Native task map
App type: Expo app using TypeScript and Expo Router.
Native runtime: Expo Go for pure JS changes, development build for native libraries.
Targets: Android emulator first, iOS simulator on macOS before release.
Allowed files: app/, components/, hooks/, app.config.ts, metro.config.js, __tests__/.
Do not change: package manager, app slug, bundle identifiers, signing files, .env files.
Verification: npm run lint, npm test, npx expo-doctor, Android emulator smoke test.
Handoff: list changed files, commands run, platform not tested, and any native rebuild needed.
Current Expo project templates may include AI-agent context, but existing apps often have old README files, old SDK assumptions, or native settings that only the repository can reveal. Have Claude Code read the real project before changing it.
Choose Expo or bare React Native deliberately
React Native’s documentation separates framework-based development from direct Android Studio and Xcode setup. In practice, Expo is usually the fastest path for new apps, while bare React Native is still common for apps with existing native SDKs, custom Gradle work, complicated Pod settings, or vendor requirements.
| Decision | Expo is a better fit | Bare React Native is a better fit |
|---|---|---|
| Starting speed | New MVPs, internal tools, learning projects, Expo SDK coverage | Existing iOS/Android code must stay in place |
| Native features | Camera, SecureStore, notifications, and settings supported by config plugins | Custom SDKs, payment terminals, uncommon Bluetooth, detailed build settings |
| Claude Code scope | Mostly app/, components/, app.config.ts, tests | ios/, android/, Codegen, Pods, Gradle, generated artifacts |
| Verification | Expo Go or development build, npx expo-doctor | npm run android, pod install, Xcode and Android Studio builds |
Do not treat Expo Go as proof that a release will work. Expo explains Expo Go as a quick environment with a fixed set of native libraries. When you add a library with native code, use a development build and tell Claude Code to report that a new binary is required.
For a clean Expo test project, use:
npx create-expo-app@latest rn-claude-lab
cd rn-claude-lab
npx expo install expo-camera expo-secure-store @react-native-community/netinfo
npm install --save-dev @testing-library/react-native jest-expo @types/jest
npx expo start
Press a for an Android Emulator or i for an iOS Simulator on macOS. If the local network blocks the device, npx expo start --tunnel can help, but it is slower than LAN or emulator workflows.
Set Claude Code permissions for mobile work
React Native projects can create large side effects from one command. expo prebuild, pod install, gradlew clean, and eas build are valid in the right task, but they should not happen silently during a small UI edit.
Claude Code permissions let you separate allow, ask, and deny rules. A shared .claude/settings.json can allow safe checks, ask before native regeneration or release builds, and deny secrets and publishing commands.
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Bash(npm test)",
"Bash(npm run lint)",
"Bash(npx expo-doctor)",
"Bash(adb devices)"
],
"ask": [
"Edit",
"Bash(npx expo prebuild*)",
"Bash(eas build*)",
"Bash(cd ios && bundle exec pod install)"
],
"deny": [
"Read(.env)",
"Read(.env.local)",
"Bash(git push*)",
"Bash(rm -rf*)"
]
}
}
This is not about distrusting the tool. It is about keeping mobile side effects reviewable. If a task needs a native rebuild, Claude Code should say so before the diff spreads into generated folders.
Build a camera permission screen
The first concrete use case is a QR scanner for check-in, inventory, or internal tools. Expo Camera currently exposes CameraView and useCameraPermissions. Ask Claude Code to model the three states explicitly: permission loading, permission denied/not granted, and camera ready. Also require accessible labels and duplicate-scan protection.
// components/CameraQrCheck.tsx
import { CameraView, useCameraPermissions } from 'expo-camera';
import { useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
type ScanResult = {
data: string;
type: string;
} | null;
export function CameraQrCheck() {
const [permission, requestPermission] = useCameraPermissions();
const [scan, setScan] = useState<ScanResult>(null);
if (!permission) {
return (
<View style={styles.center}>
<Text>Checking camera permission...</Text>
</View>
);
}
if (!permission.granted) {
return (
<View style={styles.center}>
<Text style={styles.title}>Camera access is required</Text>
<Text style={styles.body}>
Allow camera access to scan QR codes on this device.
</Text>
<Pressable
accessibilityRole="button"
accessibilityLabel="Allow camera access"
disabled={!permission.canAskAgain}
onPress={requestPermission}
style={({ pressed }) => [
styles.button,
pressed && styles.buttonPressed,
!permission.canAskAgain && styles.buttonDisabled,
]}
>
<Text style={styles.buttonText}>Allow camera</Text>
</Pressable>
</View>
);
}
return (
<View style={styles.container}>
<CameraView
style={styles.camera}
barcodeScannerSettings={{ barcodeTypes: ['qr'] }}
onBarcodeScanned={
scan
? undefined
: ({ data, type }) => {
setScan({ data, type });
}
}
/>
<View style={styles.result}>
<Text accessibilityLiveRegion="polite" style={styles.title}>
{scan ? `Scanned ${scan.type}` : 'Point the camera at a QR code'}
</Text>
{scan ? <Text selectable>{scan.data}</Text> : null}
{scan ? (
<Pressable
accessibilityRole="button"
accessibilityLabel="Scan another QR code"
onPress={() => setScan(null)}
style={styles.button}
>
<Text style={styles.buttonText}>Scan again</Text>
</Pressable>
) : null}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#111827' },
center: { flex: 1, justifyContent: 'center', gap: 12, padding: 24 },
camera: { flex: 1 },
result: { gap: 12, padding: 16, backgroundColor: '#f9fafb' },
title: { fontSize: 18, fontWeight: '700', color: '#111827' },
body: { fontSize: 15, lineHeight: 22, color: '#374151' },
button: {
alignItems: 'center',
borderRadius: 8,
backgroundColor: '#2563eb',
paddingHorizontal: 16,
paddingVertical: 12,
},
buttonPressed: { opacity: 0.75 },
buttonDisabled: { backgroundColor: '#9ca3af' },
buttonText: { color: '#ffffff', fontWeight: '700' },
});
The component is copy-pasteable in an Expo project after installing expo-camera. Native permission text belongs in app config, because some changes require rebuilding the native app.
// app.config.ts
import type { ConfigContext, ExpoConfig } from 'expo/config';
export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
name: config.name ?? 'rn-claude-lab',
slug: config.slug ?? 'rn-claude-lab',
ios: {
...config.ios,
bundleIdentifier: 'com.example.rnclaudelab',
infoPlist: {
...config.ios?.infoPlist,
NSCameraUsageDescription: 'Scan QR codes for check-in.',
},
},
android: {
...config.android,
package: 'com.example.rnclaudelab',
permissions: ['CAMERA'],
},
plugins: ['expo-camera'],
});
Expo config plugins apply native configuration during prebuild and native build flows. Tell Claude Code: “If you edit app.config.ts, report whether a development build is required.” That one instruction prevents many “works in Expo Go, fails in internal build” surprises.
Treat Metro errors as evidence, not noise
The second concrete use case is Metro’s Unable to resolve module class of failures, especially in monorepos. Metro’s official troubleshooting page includes cache reset guidance, but resetting cache should be the first cleanup step, not the whole diagnosis.
Give Claude Code the full error, command, package manager, workspace layout, and the last file move. For Expo apps inside a workspace, a Metro config may be needed:
// metro.config.js
const path = require('path');
const { getDefaultConfig } = require('expo/metro-config');
const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, '..');
const config = getDefaultConfig(projectRoot);
config.watchFolders = [workspaceRoot];
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, 'node_modules'),
path.resolve(workspaceRoot, 'node_modules'),
];
module.exports = config;
Do not add this blindly. A normal single-package Expo app usually does not need it. Ask Claude Code to explain why watchFolders is necessary, and to avoid it if the error is actually a casing mistake, missing dependency, stale Babel config, or wrong import path.
Plan native modules before editing native code
The third use case is bridging a vendor SDK or platform API. React Native’s current Native Modules guide focuses on Turbo Native Modules and Codegen. That means a good task starts with the typed JavaScript/TypeScript surface, then moves to Android and iOS implementation.
If Expo SDK modules such as Camera, SecureStore, or NetInfo are enough, do not write a custom native module. If you must integrate a payment terminal, custom Bluetooth device, or internal authentication SDK, ask for a plan first:
Implement a native bridge plan, not the full code yet.
Goal: expose a device serial reader to TypeScript.
First output:
1. TypeScript interface and error model.
2. Android/iOS files that would need edits.
3. Build commands for each platform.
4. Risks: permissions, threading, simulator limitations, release signing.
Do not edit ios/ or android/ until the plan is reviewed.
This is slower for the first ten minutes and faster for the rest of the task. Native diffs are harder to review, so the boundary should be clear before Claude Code edits Kotlin, Swift, Pods, or Gradle files.
Add tests and accessibility to the same task
React Native accessibility APIs feed assistive technologies such as VoiceOver and TalkBack. When a view is intended to be accessible, labels and roles are part of the implementation, not a polish pass.
Start with a unit test for the denied-permission state:
// __tests__/CameraQrCheck.test.tsx
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react-native';
import { CameraQrCheck } from '../components/CameraQrCheck';
const mockRequestPermission = jest.fn();
jest.mock('expo-camera', () => ({
CameraView: 'CameraView',
useCameraPermissions: () => [
{ granted: false, canAskAgain: true },
mockRequestPermission,
],
}));
describe('CameraQrCheck', () => {
beforeEach(() => {
mockRequestPermission.mockClear();
});
it('requests camera permission from the empty state', () => {
render(<CameraQrCheck />);
fireEvent.press(screen.getByText('Allow camera'));
expect(mockRequestPermission).toHaveBeenCalledTimes(1);
});
});
Then test on a device or emulator. Check the permission dialog, denial path, rotation, low-light scanning, and screen-reader labels. The handoff should say which platform was tested. “Android emulator passed, iOS not tested” is useful. “Looks good” is not.
Make release checks explicit
React Native debugging tools such as the Dev Menu and LogBox are development tools; release builds behave differently. Claude Code should return command evidence, not just a statement that the implementation is done.
npm run lint
npm test -- --runInBand
npx expo-doctor
npx expo start -c
adb devices
adb shell input keyevent 82
npx expo run:android
# macOS only:
npx expo run:ios
For EAS Build, keep development, preview, and production profiles separate. Tell Claude Code not to change bundle identifiers, Android package names, signing files, or production profiles unless the task explicitly says so.
Practical use cases and failure modes
Use case one is a new Expo MVP. Login, QR scanning, local secure storage, simple API integration, and a small admin workflow can be split into screens, hooks, tests, and config. If the app supports revenue, include analytics and CTA behavior in the done criteria instead of adding them later.
Use case two is fixing an existing bare React Native app. Metro errors, Android-only crashes, iOS permission text bugs, and native SDK upgrades are good Claude Code tasks when you require error classification, minimal diffs, and a clear statement on whether native rebuilds are needed.
Use case three is pre-implementation research for a native SDK. Payments, health data, Bluetooth, and enterprise authentication deserve a table of supported OS versions, permissions, store-review risk, simulator limitations, and required test devices before implementation starts. The review workflow checklist and TDD guide pair well with this.
The recurring pitfalls are predictable. Expo Go success is not release proof. Cache reset is not a root-cause analysis. Platform checks cannot wait until the end. Accessibility labels should not be bolted on after UI review. Native build commands should not run without a reason. These are process problems, not AI problems.
CTA: Turn the workflow into a template
For React Native, the highest leverage is a reusable project map, permission file, verification command list, and review checklist. Solo builders can start with the free cheatsheet. If you want reusable prompts and setup material, browse the ClaudeCodeLab products. Teams that need Expo/bare React Native rules, CI checks, release gates, and training around a real repository should use Claude Code training and consultation.
For adjacent reading, compare this with the Claude Code React development guide and the Flutter/Dart guide.
After trying this workflow in practice, Masa found that three habits reduced rework the most: deciding Expo versus bare before implementation, requiring a development-build note whenever app.config.ts changes, and testing permissions on Android Emulator before expanding the diff. For native features such as Camera and SecureStore, the verification boundary mattered more than the generated code speed.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Obsidian to CLAUDE.md Workflow: Stop Re-explaining Context
Turn Obsidian working notes into concise CLAUDE.md operating notes that make Claude Code sessions easier to resume.
Claude Code Revenue CTA Routing: Send Articles to PDF, Gumroad, and Consultation
A Claude Code workflow for routing article readers to the free PDF, Gumroad products, or consultation by intent.
Claude Code Team Handoff Rules: Review Evidence, Permissions, Rollback, and Revenue Paths
A practical Claude Code handoff format for team review, proof, permission rules, rollback, free PDF, Gumroad, and consultation paths.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.