skills/0xkynz/codekit/react-native-expo

react-native-expo

SKILL.md

React Native Development Expert

Expert in React Native development with Expo, TypeScript, and modern mobile tooling. Specialized in building performant cross-platform mobile applications with best practices.

When to Use

  • React Native projects (Expo or bare workflow)
  • Cross-platform mobile applications (iOS & Android)
  • Mobile apps with native functionality
  • Projects requiring native device features

For web-only React projects, use react agent instead.

Technology Stack

Core

  • React Native: Cross-platform mobile framework
  • Expo SDK 52+: Managed workflow and native APIs
  • TypeScript: Strict typing and best practices
  • Expo Router: File-based navigation

UI/Styling

  • NativeWind: Tailwind CSS for React Native
  • React Native Reanimated: Smooth animations
  • React Native Gesture Handler: Touch interactions
  • Expo Vector Icons: Icon library

Navigation

  • Expo Router: File-based routing (recommended)
  • React Navigation: Stack, Tab, Drawer navigators

Data & State

  • TanStack Query: Server state management
  • Zustand: Client state management
  • React Hook Form + Zod: Form handling
  • MMKV: Fast key-value storage
  • Expo SecureStore: Secure data storage

Native APIs

  • Expo Camera: Camera access
  • Expo Notifications: Push notifications
  • Expo Location: Geolocation
  • Expo Image Picker: Media selection
  • Expo FileSystem: File operations

Project Structure

/my-react-native-app
├── /app/                     # Expo Router screens
│   ├── (tabs)/               # Tab navigator group
│   │   ├── index.tsx         # Home tab
│   │   ├── profile.tsx       # Profile tab
│   │   └── _layout.tsx       # Tab layout
│   ├── (auth)/               # Auth screens group
│   │   ├── login.tsx
│   │   ├── register.tsx
│   │   └── _layout.tsx
│   ├── [id].tsx              # Dynamic route
│   ├── _layout.tsx           # Root layout
│   └── +not-found.tsx        # 404 screen
├── /src/
│   ├── /components/          # Reusable components
│   │   ├── /ui/              # Base UI (Button, Input, Card)
│   │   ├── /forms/           # Form components
│   │   └── /lists/           # List components
│   ├── /features/            # Feature modules
│   │   ├── /auth/
│   │   │   ├── /components/
│   │   │   ├── /hooks/
│   │   │   ├── /services/
│   │   │   └── index.ts
│   │   └── /settings/
│   ├── /hooks/               # Custom hooks
│   ├── /services/            # API services
│   ├── /store/               # State management
│   ├── /types/               # TypeScript types
│   ├── /utils/               # Utilities
│   ├── /constants/           # App constants
│   └── /theme/               # Theme configuration
├── /assets/                  # Images, fonts, etc.
├── app.json                  # Expo config
├── eas.json                  # EAS Build config
├── tailwind.config.js        # NativeWind config
├── tsconfig.json
└── package.json

Code Standards

Component Pattern

import { View, Text, Pressable } from "react-native";
import { forwardRef } from "react";
import { cn } from "@/utils/cn";

interface ButtonProps {
  variant?: "default" | "outline" | "ghost";
  size?: "sm" | "md" | "lg";
  onPress?: () => void;
  disabled?: boolean;
  className?: string;
  children: React.ReactNode;
}

const Button = forwardRef<View, ButtonProps>(
  ({ variant = "default", size = "md", className, children, ...props }, ref) => {
    return (
      <Pressable
        ref={ref}
        className={cn(
          "items-center justify-center rounded-lg",
          variants[variant],
          sizes[size],
          props.disabled && "opacity-50",
          className
        )}
        {...props}
      >
        <Text className={cn("font-medium", textVariants[variant])}>
          {children}
        </Text>
      </Pressable>
    );
  }
);
Button.displayName = "Button";

export { Button };

Custom Hook Pattern

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

export function useUsers() {
  return useQuery({
    queryKey: ["users"],
    queryFn: () => userService.getAll(),
  });
}

export function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: userService.create,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["users"] });
    },
  });
}

Form Pattern (React Hook Form + Zod)

import { View, TextInput, Text, Pressable } from "react-native";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

type FormData = z.infer<typeof schema>;

export function LoginForm() {
  const { control, handleSubmit, formState: { errors } } = useForm<FormData>({
    resolver: zodResolver(schema),
  });

  const onSubmit = (data: FormData) => {
    // Handle submission
  };

  return (
    <View className="gap-4">
      <Controller
        control={control}
        name="email"
        render={({ field: { onChange, onBlur, value } }) => (
          <View>
            <TextInput
              className="border border-gray-300 rounded-lg px-4 py-3"
              placeholder="Email"
              onBlur={onBlur}
              onChangeText={onChange}
              value={value}
              keyboardType="email-address"
              autoCapitalize="none"
            />
            {errors.email && (
              <Text className="text-red-500 text-sm mt-1">
                {errors.email.message}
              </Text>
            )}
          </View>
        )}
      />
      <Controller
        control={control}
        name="password"
        render={({ field: { onChange, onBlur, value } }) => (
          <View>
            <TextInput
              className="border border-gray-300 rounded-lg px-4 py-3"
              placeholder="Password"
              onBlur={onBlur}
              onChangeText={onChange}
              value={value}
              secureTextEntry
            />
            {errors.password && (
              <Text className="text-red-500 text-sm mt-1">
                {errors.password.message}
              </Text>
            )}
          </View>
        )}
      />
      <Pressable
        className="bg-blue-500 rounded-lg py-3 items-center"
        onPress={handleSubmit(onSubmit)}
      >
        <Text className="text-white font-semibold">Login</Text>
      </Pressable>
    </View>
  );
}

Expo Router Layout

// app/_layout.tsx
import { Stack } from "expo-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import "../global.css";

const queryClient = new QueryClient();

export default function RootLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <QueryClientProvider client={queryClient}>
        <Stack>
          <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
          <Stack.Screen name="(auth)" options={{ headerShown: false }} />
        </Stack>
      </QueryClientProvider>
    </GestureHandlerRootView>
  );
}

Tab Navigator Layout

// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
import { Ionicons } from "@expo/vector-icons";

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: "#3b82f6",
        tabBarInactiveTintColor: "#9ca3af",
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: "Home",
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: "Profile",
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

List with FlashList

import { FlashList } from "@shopify/flash-list";
import { View, Text, Pressable } from "react-native";

interface User {
  id: string;
  name: string;
  email: string;
}

interface UsersListProps {
  users: User[];
  onUserPress: (user: User) => void;
}

export function UsersList({ users, onUserPress }: UsersListProps) {
  const renderItem = ({ item }: { item: User }) => (
    <Pressable
      className="bg-white p-4 border-b border-gray-100"
      onPress={() => onUserPress(item)}
    >
      <Text className="font-semibold text-gray-900">{item.name}</Text>
      <Text className="text-gray-500 text-sm">{item.email}</Text>
    </Pressable>
  );

  return (
    <FlashList
      data={users}
      renderItem={renderItem}
      estimatedItemSize={72}
      keyExtractor={(item) => item.id}
    />
  );
}

Animation Pattern (Reanimated)

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  withTiming,
} from "react-native-reanimated";
import { Pressable } from "react-native";

export function AnimatedButton({ children, onPress }) {
  const scale = useSharedValue(1);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
  }));

  const handlePressIn = () => {
    scale.value = withSpring(0.95);
  };

  const handlePressOut = () => {
    scale.value = withSpring(1);
  };

  return (
    <Pressable
      onPressIn={handlePressIn}
      onPressOut={handlePressOut}
      onPress={onPress}
    >
      <Animated.View style={animatedStyle}>{children}</Animated.View>
    </Pressable>
  );
}

App Configuration

// app.json
{
  "expo": {
    "name": "My App",
    "slug": "my-app",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "scheme": "myapp",
    "userInterfaceStyle": "automatic",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "assetBundlePatterns": ["**/*"],
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.company.myapp"
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      },
      "package": "com.company.myapp"
    },
    "plugins": [
      "expo-router",
      "expo-secure-store",
      [
        "expo-camera",
        {
          "cameraPermission": "Allow $(PRODUCT_NAME) to access your camera"
        }
      ]
    ]
  }
}

Best Practices

  1. Component Organization

    • Use feature-based folder structure
    • Colocate related code (components, hooks, types)
    • Use barrel exports (index.ts)
    • Keep components small and focused
  2. State Management

    • Server state: TanStack Query
    • Client state: Zustand
    • Form state: React Hook Form
    • Navigation state: Expo Router
    • Persistent state: MMKV or SecureStore
  3. Performance

    • Use FlashList instead of FlatList for long lists
    • Avoid inline styles and functions in render
    • Use React.memo for expensive components
    • Implement skeleton loaders for async content
    • Use Reanimated for smooth animations
  4. Styling

    • Use NativeWind for Tailwind-like styling
    • Support dark mode via useColorScheme
    • Use consistent spacing and typography
    • Handle safe areas with SafeAreaView
  5. TypeScript

    • Define interfaces for all props
    • Use strict mode
    • Type navigation params properly
    • Use Zod for runtime validation
  6. Platform Handling

    • Use Platform.select() for platform-specific code
    • Create .ios.tsx and .android.tsx files when needed
    • Test on both platforms regularly
    • Handle keyboard avoidance properly
  7. Accessibility

    • Add accessibilityLabel to interactive elements
    • Use accessibilityRole appropriately
    • Ensure adequate touch target sizes (44x44 minimum)
    • Support dynamic text sizes
  8. Error Handling

    • Implement error boundaries
    • Handle network errors gracefully
    • Show meaningful error messages
    • Add retry mechanisms for failed requests

Quick Setup Commands

# Create new Expo project
npx create-expo-app@latest my-app --template tabs
cd my-app

# Install core dependencies
npx expo install @tanstack/react-query
npm install zustand
npm install react-hook-form @hookform/resolvers zod

# Install UI/Animation
npx expo install react-native-reanimated react-native-gesture-handler
npm install nativewind tailwindcss

# Install FlashList for performant lists
npx expo install @shopify/flash-list

# Install storage
npx expo install react-native-mmkv expo-secure-store

# Initialize NativeWind
npx tailwindcss init

# Start development
npx expo start

EAS Build Configuration

// eas.json
{
  "cli": {
    "version": ">= 5.0.0"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal"
    },
    "preview": {
      "distribution": "internal"
    },
    "production": {}
  },
  "submit": {
    "production": {}
  }
}
# Build for development
eas build --profile development --platform ios

# Build for production
eas build --profile production --platform all

# Submit to stores
eas submit --platform ios
eas submit --platform android
Weekly Installs
8
Repository
0xkynz/codekit
GitHub Stars
1
First Seen
13 days ago
Installed on
opencode8
claude-code8
github-copilot8
codex8
amp8
cline8