← All posts
·12 min read

Claude Code with React Native and Expo

Claude CodeReact NativeWorkflowMobile
Claude Code with React Native and Expo

Why Claude Code needs a different setup for React Native

Claude Code understands React Native. It knows StyleSheet, FlatList, useColorScheme, SafeAreaView, Expo SDK modules, and the general shape of a cross-platform mobile app. What it cannot infer without configuration is your project: which navigation library you are using and at what version, how your file-based routing is structured, whether you are on the bare workflow or managed Expo, which native modules are installed and how they are initialized, and what your build pipeline looks like.

The gap between "Claude knows React Native" and "Claude writes React Native code that fits your codebase" is entirely closed by a well-written CLAUDE.md. Without it, Claude generates components using the wrong navigation API, imports modules from the wrong package names, writes styles inline instead of using your StyleSheet or utility library, and misses the platform-specific file convention (Component.ios.tsx, Component.android.tsx).

React Native's split between Expo managed workflow, Expo bare workflow, and plain React Native CLI adds a second layer of ambiguity. Claude has to know which you are using before it can generate any non-trivial code. One declaration in CLAUDE.md eliminates a category of wrong outputs.

If you are starting from scratch, the Claude Code setup guide covers installation and authentication before any framework-specific configuration applies.

The React Native CLAUDE.md

The root CLAUDE.md for a React Native project needs to answer: what workflow are you on, which SDK version, how is navigation structured, what is the file naming convention, what native modules are installed, how do you run and test the app, and what are the hard rules?

# React Native / Expo project rules

## Stack
- Framework: Expo SDK 53 (managed workflow)
- Language: TypeScript 5.x, strict mode
- Navigation: Expo Router v4 (file-based routing)
- State: Zustand 5.x for global state
- Styling: NativeWind v4 (Tailwind for React Native)
- Forms: React Hook Form + Zod
- Data fetching: TanStack Query v5

## Project structure
- `app/`, Expo Router file-based routes
  - `(tabs)/`, tab bar routes
  - `(auth)/`, auth flow routes
  - `_layout.tsx`, root layout, providers, navigation config
- `components/`, shared UI components
- `hooks/`, custom hooks, typed
- `stores/`, Zustand stores
- `lib/`, utilities, API client, helpers
- `assets/`, images, fonts, icons

## File naming conventions
- Components: PascalCase.tsx
- Hooks: useCamelCase.ts
- Stores: camelCaseStore.ts
- Platform-specific: Component.ios.tsx / Component.android.tsx
- Route files follow Expo Router conventions (lowercase with hyphens)

## Native modules installed
- expo-camera (camera access)
- expo-location (GPS, background location)
- expo-notifications (push notifications, backend: Expo Push Service)
- expo-secure-store (encrypted key-value storage)
- expo-image-picker (gallery + camera selection)
- react-native-reanimated v3
- react-native-gesture-handler v2

## Running the project
- iOS simulator: `npx expo start --ios`
- Android emulator: `npx expo start --android`
- Physical device: `npx expo start` then scan QR
- Production build: EAS Build (see build commands below)
- Never use `react-native run-ios`, use expo CLI

## Testing
- Unit/component: Jest + React Native Testing Library (RNTL)
- E2E: Maestro (YAML-based, runs on device/simulator)
- Run unit tests: `npx jest`
- Run single file: `npx jest path/to/Component.test.tsx`
- No Detox, project uses Maestro for E2E

## Hard rules
- No inline styles. Use NativeWind classes or StyleSheet.create().
- No React Navigation imports. All navigation uses Expo Router hooks (useRouter, useLocalSearchParams, Link).
- All async operations are wrapped in try/catch with user-visible error handling.
- No Platform.OS checks in shared components, use platform-specific files instead.
- All text is wrapped in <Text> components. No bare string literals in JSX.
- expo-secure-store for any sensitive data. Never AsyncStorage for tokens.

Three declarations here do the most work.

The navigation rule prevents the most common mistake: importing from @react-navigation/native when you are on Expo Router. The two APIs are not interchangeable. Expo Router uses useRouter() and useLocalSearchParams() from expo-router; React Navigation uses useNavigation() and useRoute(). One wrong import and the component breaks at runtime. The explicit rule means Claude never generates the wrong import.

The Platform.OS rule enforces a better architecture. Instead of {Platform.OS === 'ios' ? <IOSThing /> : <AndroidThing />} scattered through components, platform-specific logic goes in Component.ios.tsx and Component.android.tsx files. Metro bundler picks the right one at build time. Claude will generate platform-specific files when this convention is declared.

The NativeWind rule prevents raw StyleSheet objects appearing alongside utility classes. Pick one approach per project and declare it; Claude will use it consistently.

Expo Router file conventions

Expo Router's file-based routing is where Claude Code needs the most specific guidance. The routing conventions are different from both React Navigation and Next.js's App Router, so Claude needs explicit examples to generate route files correctly.

Expand your CLAUDE.md with an Expo Router section:

## Expo Router conventions

### Route file patterns
app/
  _layout.tsx              # Root layout, Stack or Tabs navigator
  index.tsx                # Home route (/)
  (tabs)/
    _layout.tsx            # Tab bar layout
    index.tsx              # First tab
    profile.tsx            # /profile tab
  (auth)/
    _layout.tsx            # Auth stack layout
    sign-in.tsx            # /sign-in
    sign-up.tsx            # /sign-up
  [id]/
    index.tsx              # Dynamic route (/123)
    details.tsx            # /123/details
  +not-found.tsx           # 404 handler

### Navigation in components
import { useRouter, useLocalSearchParams, Link } from 'expo-router';

// Programmatic navigation:
const router = useRouter();
router.push('/profile');
router.replace('/(auth)/sign-in');
router.back();

// Dynamic routes:
router.push({ pathname: '/[id]', params: { id: '123' } });

// Typed params:
const { id } = useLocalSearchParams<{ id: string }>();

// Declarative:
<Link href="/profile">Profile</Link>
<Link href={{ pathname: '/[id]', params: { id: item.id } }}>View</Link>

### Layout components
// Stack layout in _layout.tsx:
import { Stack } from 'expo-router';
export default function Layout() {
  return <Stack screenOptions={{ headerShown: false }} />;
}

// Tabs layout:
import { Tabs } from 'expo-router';
export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen name="index" options={{ title: 'Home' }} />
      <Tabs.Screen name="profile" options={{ title: 'Profile' }} />
    </Tabs>
  );
}

With these examples in CLAUDE.md, Claude generates route files that follow your exact conventions. It creates _layout.tsx files with the right navigator type, uses useRouter() instead of useNavigation(), and generates dynamic route files with the correct bracket notation.

The grouped route convention ((tabs), (auth)) is particularly easy to get wrong. Claude will generate these with and without parentheses inconsistently without the examples above. The pattern makes the expected structure unambiguous.

Metro bundler configuration

Claude Code can modify your Metro config when you ask it to add aliases, custom resolver options, or transform rules. Without knowing your existing metro.config.js, it will generate a config that overwrites your current one. Put the key aspects of your Metro setup in CLAUDE.md:

## Metro config (metro.config.js)
- Base config from: @expo/metro-config
- Path aliases configured: @/components, @/hooks, @/lib, @/stores
- SVG transformer: react-native-svg-transformer
- Do NOT remove SVG transformer when editing metro.config.js
- Do NOT change sourceExts order, it affects resolution priority

## tsconfig.json path mapping
{
  "paths": {
    "@/*": ["./*"]
  }
}
// Claude should use @/ aliases in all new imports, not relative paths

The SVG transformer note prevents a specific failure: when Claude adds a new Metro transform rule, it will sometimes overwrite the transformer config block, removing the SVG transformer in the process. An explicit "do not remove" instruction in CLAUDE.md stops this.

The path alias instruction also changes Claude's import generation. With @/ aliases declared, Claude writes import { Button } from '@/components/Button' rather than import { Button } from '../../../components/Button'. This is a significant quality-of-life improvement in generated code.

Native module patterns

Expo's native modules require initialization and permission handling that Claude needs to understand to write correct code. Without this context, Claude will write camera or location code that calls the API directly without checking permissions first.

Add to CLAUDE.md:

## Native module patterns

### Permissions (mandatory before any native module use)
import { useCameraPermissions } from 'expo-camera';
import { requestForegroundPermissionsAsync } from 'expo-location';

// Camera pattern:
const [permission, requestPermission] = useCameraPermissions();
if (!permission?.granted) {
  return <PermissionPrompt onRequest={requestPermission} />;
}

// Location pattern:
const { status } = await requestForegroundPermissionsAsync();
if (status !== 'granted') {
  // Handle denied state
  return;
}

### Push notifications setup
// Always check + request before subscribing:
const { status: existingStatus } = await getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
  const { status } = await requestPermissionsAsync();
  finalStatus = status;
}
if (finalStatus !== 'granted') return;

// Get token:
const token = await getExpoPushTokenAsync({ projectId: Constants.expoConfig.extra.eas.projectId });

### Secure storage
import * as SecureStore from 'expo-secure-store';

// Auth tokens:
await SecureStore.setItemAsync('auth_token', token);
const token = await SecureStore.getItemAsync('auth_token');
await SecureStore.deleteItemAsync('auth_token');

// Never: AsyncStorage.setItem('auth_token', token), not encrypted

The permissions pattern is the most important. Every native module in Expo that touches hardware (camera, location, notifications, contacts, biometrics) requires a permission request before the API is available. Claude will write the API calls correctly but will skip the permission check when not explicitly told to include it. The CLAUDE.md pattern means every generated component that uses a native module starts with the permission check.

The push notification token pattern needs the projectId from Constants.expoConfig rather than a hardcoded string. This is the correct way to get the EAS project ID at runtime. Claude will sometimes hardcode a placeholder string here without guidance.

The Claude Code best practices guide covers the general principle behind these CLAUDE.md patterns: the more precisely you define your project's conventions, the less Claude has to guess. For the React layer specifically, the Claude Code React guide covers component patterns that transfer directly into React Native screens and shared logic.

EAS Build and permission hooks

React Native production builds run through EAS Build. Claude Code can trigger builds via the eas CLI, but you want control over which commands run automatically versus which require your confirmation.

In .claude/settings.local.json:

{
  "permissions": {
    "allow": [
      "Bash(npx expo start*)",
      "Bash(npx jest*)",
      "Bash(eas build:inspect*)",
      "Bash(eas diagnostics*)",
      "Bash(eas whoami*)",
      "Bash(npx expo doctor*)",
      "Bash(npx expo install*)"
    ],
    "deny": [
      "Bash(eas build*)",
      "Bash(eas submit*)",
      "Bash(eas update*)",
      "Bash(eas secret*)"
    ]
  }
}

This setup lets Claude run local dev server, tests, diagnostics, and package installation freely. It blocks any command that triggers a cloud build, pushes an OTA update, submits to an app store, or touches secrets. These are irreversible or slow operations that should always require your explicit instruction.

eas build:inspect stays allowed because it only shows what a build would do without running it, which is useful for debugging configuration issues.

For how permission hooks work across any project, the Claude Code hooks guide covers the full hook system and what other commands are worth gating.

Zustand store patterns

Claude Code generates clean Zustand stores when given the pattern, but will vary the syntax across create, createStore, and subscribeWithSelector without guidance. Define your preferred store shape in CLAUDE.md:

## Zustand store conventions

### Basic store pattern (stores/authStore.ts)
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

interface AuthState {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
  setUser: (user: User, token: string) => void;
  clearAuth: () => void;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      user: null,
      token: null,
      isAuthenticated: false,
      setUser: (user, token) => set({ user, token, isAuthenticated: true }),
      clearAuth: () => set({ user: null, token: null, isAuthenticated: false }),
    }),
    {
      name: 'auth-storage',
      storage: createJSONStorage(() => AsyncStorage),
    }
  )
);

// Usage in component:
const { user, isAuthenticated, setUser } = useAuthStore();

Note: For auth tokens, use expo-secure-store directly, not Zustand persist. The Zustand store above persists non-sensitive user data (display name, preferences).


The persist middleware note is important. AsyncStorage is acceptable for non-sensitive user preferences but not for auth tokens. The comment in CLAUDE.md ensures Claude generates the right storage layer for each type of data.

## TanStack Query with React Native

Data fetching in React Native with TanStack Query follows the same patterns as web, but needs React Native-specific configuration for background fetches, app state refetching, and offline handling.

Add to CLAUDE.md:

```markdown
## TanStack Query setup (lib/queryClient.ts)
import { QueryClient } from '@tanstack/react-query';
import { focusManager, onlineManager } from '@tanstack/react-query';
import NetInfo from '@react-native-community/netinfo';
import { AppState } from 'react-native';

// Online state from NetInfo (not browser navigator.onLine):
onlineManager.setEventListener((setOnline) => {
  return NetInfo.addEventListener((state) => {
    setOnline(!!state.isConnected);
  });
});

// Refetch on app foreground (replaces window focus listener):
focusManager.setEventListener((handleFocus) => {
  const subscription = AppState.addEventListener('change', (state) => {
    handleFocus(state === 'active');
  });
  return () => subscription.remove();
});

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5,    // 5 minutes
      retry: 2,
      refetchOnWindowFocus: false,  // handled by AppState above
    },
  },
});

Without the onlineManager and focusManager setup in CLAUDE.md, Claude will generate TanStack Query configuration that uses browser APIs (window focus, navigator.onLine) which do not exist in React Native. The React Native equivalents require NetInfo and AppState, and they need explicit wiring. One configuration block in CLAUDE.md makes every generated query hook work correctly in a native context.

What React Native developers get wrong first

Three configuration mistakes come up consistently when React Native developers start using Claude Code.

Not declaring the Expo SDK version. Claude knows both Expo SDK 50, 51, and 52/53. The module APIs changed across versions. expo-camera v14 (SDK 52+) uses the new hook-based API; earlier versions use the class-based Camera component. Without the SDK version in CLAUDE.md, Claude will pick whichever pattern was most common in its training data, which may not match your installed version.

Missing the NativeWind version. NativeWind v2 and v4 have completely different setup and API. v2 uses Babel plugin setup; v4 uses a Metro transformer. The className prop behavior differs. Declare NativeWind v4 explicitly and the right patterns follow.

Not specifying Reanimated worklet conventions. react-native-reanimated v3 uses a specific worklet model where functions running on the UI thread must be declared with 'worklet'. Claude knows this but will generate Reanimated code without the worklet directive when working in files that do not clearly signal animation context. A brief note in CLAUDE.md ("All Reanimated animated functions require 'worklet' directive") prevents the silent runtime crash.

Getting more from your React Native workflow

The configuration in this guide gives Claude Code the context it needs to generate React Native components that follow your navigation conventions, use the right native module permission patterns, stay within your state management architecture, and handle the cross-platform split correctly with platform-specific files.

The underlying principle is the same one that applies to any Claude Code workflow: the agent works from its context window, and your CLAUDE.md is the highest-leverage input you have. A React Native project with the CLAUDE.md above gets generated components, screens, hooks, and stores that fit the project structure. A project without it gets generic JavaScript that needs significant rework for mobile.

Start with the stack declaration and navigation convention sections, since those affect every file Claude touches. Add the native module patterns and EAS Build permissions once those are in regular use. If you want Claude Code handling your full mobile development cycle, from new screens to component refactors to test generation, Claudify includes a React Native CLAUDE.md template pre-configured for Expo SDK 53, Expo Router v4, NativeWind v4, and EAS Build workflows.

More like this

Ready to upgrade your Claude Code setup?

Get Claudify
Featured on Dofollow.Tools AI Toolz Dir