mobile-frontend
React Native patterns, NativeWind styling, React Native Reusables components, mobile-specific patterns
Best use case
mobile-frontend is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
React Native patterns, NativeWind styling, React Native Reusables components, mobile-specific patterns
Teams using mobile-frontend should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/mobile-frontend/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How mobile-frontend Compares
| Feature / Agent | mobile-frontend | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
React Native patterns, NativeWind styling, React Native Reusables components, mobile-specific patterns
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Mobile Frontend Skill
## Core Principles
1. **Every Text Must Be Styled** - React Native doesn't cascade styles; every Text component needs direct className
2. **Event-Driven Hooks** - Subscribe to specific events only, not all events
3. **Shared Service Instances** - Use ActivityRecorderProvider for single service instance across components
4. **Semantic Colors** - Always use design tokens (text-foreground, bg-background, etc.)
5. **Consume-Once Navigation** - Use activitySelectionStore for complex object navigation instead of URL params
6. **Platform-Specific Code** - Use NativeWind classes (ios:pt-12 android:pt-6) for platform differences
## Patterns to Follow
### Pattern 1: Text Styling (CRITICAL)
**When to use**: Every single Text component in the app
**Why**: React Native has no style inheritance
```tsx
// ❌ BAD - Text inherits nothing
<View className="text-foreground">
<Text>This text has no color!</Text>
</View>
// ✅ GOOD - Direct styling on every Text
<View className="bg-background">
<Text className="text-foreground font-semibold">Title</Text>
<Text className="text-muted-foreground text-sm">Subtitle</Text>
</View>
// ✅ GOOD - Semantic color variants
<Text variant="h1" className="text-foreground">Heading</Text>
<Text variant="p" className="text-foreground">Paragraph</Text>
<Text variant="muted" className="text-muted-foreground">Secondary</Text>
```
**Key Points**:
- Style every Text element explicitly
- Use semantic color classes: `text-foreground`, `text-muted-foreground`, `text-destructive`
- Use variant prop when available on Text component
- TextClassContext can provide default styling in custom components
### Pattern 2: React Native Reusables Icon Usage
**When to use**: Any time you need an icon
**Why**: Provides consistent styling and theming
```tsx
import { Icon } from '@/components/ui/icon';
import { Home, Activity, Settings } from 'lucide-react-native';
// ✅ CORRECT - Icon wrapper component
<Icon as={Home} size={24} className="text-foreground" />
// ✅ CORRECT - In Button
<Button variant="default">
<Icon as={Activity} size={18} />
<Text className="text-primary-foreground">Start</Text>
</Button>
// ❌ WRONG - Direct icon usage
<Home size={24} /> {/* No styling context */
```
**Key Points**:
- Always use `<Icon as={IconComponent} />` pattern
- Icons automatically get theme colors
- Can override with className
- Works inside Button/Card components
### Pattern 3: Shared Service via Provider
**When to use**: ActivityRecorder service across multiple screens
**Why**: Ensures single service instance, prevents state fragmentation
```tsx
// ✅ GOOD - Single shared instance
// In recording layout (_layout.tsx)
<ActivityRecorderProvider profile={profile}>
<Stack />
</ActivityRecorderProvider>;
// In any screen
function RecordScreen() {
const service = useSharedActivityRecorder(); // Same instance everywhere
const state = useRecordingState(service);
// ...
}
// ❌ BAD - Multiple instances
function Screen1() {
const service = useActivityRecorder(profile); // Different instance
}
function Screen2() {
const service = useActivityRecorder(profile); // Different instance
}
```
**Key Points**:
- Wrap recording screens with ActivityRecorderProvider
- Use `useSharedActivityRecorder()` to access service
- Service state persists across navigation
- Automatic cleanup on unmount
### Pattern 4: Event-Driven Hook Subscriptions
**When to use**: Subscribing to ActivityRecorder events
**Why**: Prevents unnecessary re-renders, optimizes performance
```tsx
// ✅ GOOD - Subscribe to specific event
export function useRecordingState(
service: ActivityRecorderService | null,
): RecordingState {
const [state, setState] = useState<RecordingState>(
service?.state ?? "pending",
);
useEffect(() => {
if (!service) return;
setState(service.state);
const subscription = service.addListener("stateChanged", (newState) => {
setState(newState);
});
return () => subscription.remove(); // Always clean up
}, [service]);
return state;
}
// ❌ BAD - Subscribe to all events
useEffect(() => {
const sub = service.onAnyEvent(() => {
setNeedsUpdate(true); // Re-render entire component on any event
});
return () => sub.remove();
}, [service]);
```
**Key Points**:
- Use specific hooks: `useRecordingState`, `useCurrentReadings`, `useSessionStats`
- Each hook subscribes to specific events only
- Always return cleanup function
- Component only re-renders when subscribed values change
### Pattern 5: Consume-Once Navigation Store
**When to use**: Navigating with complex objects (activity plans, activity selection)
**Why**: URL params can't encode complex objects
```tsx
// ✅ GOOD - Use selection store
// In source screen
activitySelectionStore.setSelection({ category: "run", location: "outdoor" });
router.push("/(internal)/record");
// In destination screen
const selection = activitySelectionStore.peekSelection();
if (selection) {
service.selectActivityFromPayload(selection);
activitySelectionStore.consumeSelection(); // Clear after use
}
// ❌ BAD - URL params for complex objects
router.push({
pathname: "/record",
params: { plan: activityPlan }, // Serialization fails
});
```
**Key Points**:
- Store complex objects before navigation
- Consume once in destination screen
- Clear selection after reading
- Handles navigation back gracefully
### Pattern 6: NativeWind Platform-Specific Styling
**When to use**: Different styling for iOS vs Android
**Why**: Platform-specific design guidelines
```tsx
// ✅ GOOD - Platform variants
<View className="ios:pt-12 android:pt-6 bg-background">
<Text className="ios:text-lg android:text-base text-foreground">
Platform Text
</Text>
</View>;
// ✅ GOOD - Safe area handling
import { useSafeAreaInsets } from "react-native-safe-area-context";
function Screen() {
const insets = useSafeAreaInsets();
return (
<View style={{ paddingTop: insets.top }} className="flex-1 bg-background">
{/* Content */}
</View>
);
}
```
**Key Points**:
- Use `ios:` and `android:` prefixes
- Combine with safe area insets for notch handling
- Platform.select() for complex conditional logic
### Pattern 7: Form Mutation with Retry
**When to use**: Creating/updating data with forms
**Why**: Automatic retry, error handling, field error mapping
```tsx
import { useFormMutation } from "@/lib/hooks/useFormMutation";
const mutation = useFormMutation({
mutationFn: async (data) => trpc.activities.create.mutate(data),
form, // React Hook Form instance (optional)
invalidateQueries: [["activities"]],
successMessage: "Activity created!",
retryAttempts: 2,
onSuccess: () => router.back(),
onError: (error) => {
// Field errors automatically mapped to form
toast.error(error.message);
},
});
<Button
onPress={form.handleSubmit(mutation.mutate)}
disabled={mutation.isLoading}
>
<Text className="text-primary-foreground">
{mutation.isLoading ? "Creating..." : "Create"}
</Text>
</Button>;
```
**Key Points**:
- Automatic network error retry with exponential backoff
- Field errors mapped to React Hook Form
- Cache invalidation on success
- Loading/success/error states built-in
### Pattern 8: Memoized List Items
**When to use**: FlatList with many items
**Why**: Prevents unnecessary re-renders
```tsx
import { memo } from "react";
export const ActivityListItem = memo(
({ activity, onPress }: Props) => {
return (
<TouchableOpacity onPress={onPress}>
<Text className="text-foreground">{activity.name}</Text>
</TouchableOpacity>
);
},
(prev, next) => {
// Custom comparison - return true if equal (DON'T re-render)
return (
prev.activity.id === next.activity.id &&
prev.activity.name === next.activity.name &&
prev.activity.distance_meters === next.activity.distance_meters
);
},
);
ActivityListItem.displayName = "ActivityListItem";
// Usage in FlatList
<FlatList
data={activities}
renderItem={({ item }) => (
<ActivityListItem activity={item} onPress={handlePress} />
)}
keyExtractor={(item) => item.id}
/>;
```
**Key Points**:
- Use React.memo with custom comparison
- Compare only fields that affect rendering
- Set displayName for debugging
- Use with FlatList for best performance
## Anti-Patterns to Avoid
### Anti-Pattern 1: Multiple Service Instances
**Problem**: Each component gets different service, sensors not shared
```tsx
// ❌ BAD
function Screen1() {
const service = useActivityRecorder(profile); // Instance A
}
function Screen2() {
const service = useActivityRecorder(profile); // Instance B - different!
}
// ✅ CORRECT
<ActivityRecorderProvider profile={profile}>
<Screen1 />
<Screen2 />
</ActivityRecorderProvider>;
function Screen1() {
const service = useSharedActivityRecorder(); // Same instance
}
function Screen2() {
const service = useSharedActivityRecorder(); // Same instance
}
```
### Anti-Pattern 2: Forgetting Subscription Cleanup
**Problem**: Memory leaks from event listeners
```tsx
// ❌ BAD
useEffect(() => {
service.addListener("stateChanged", handleStateChange);
// Missing cleanup!
}, [service]);
// ✅ CORRECT
useEffect(() => {
const subscription = service.addListener("stateChanged", handleStateChange);
return () => subscription.remove(); // Always clean up
}, [service]);
```
### Anti-Pattern 3: Over-Subscribing to Events
**Problem**: Component re-renders on every event
```tsx
// ❌ BAD
useEffect(() => {
const sub = service.onAnyEvent(() => {
setNeedsUpdate(true); // Re-render on ANY event
});
return () => sub.remove();
}, [service]);
// ✅ CORRECT
const readings = useCurrentReadings(service); // Only re-renders on sensor updates
const stats = useSessionStats(service); // Only re-renders on stat changes
```
### Anti-Pattern 4: Missing TextClassContext in Custom Components
**Problem**: Text inside custom button has no styling
```tsx
// ❌ BAD
function CustomButton({ children }) {
return (
<Pressable>
<Text>{children}</Text> {/* No styling context */}
</Pressable>
);
}
// ✅ CORRECT
function CustomButton({ children, textClassName }) {
return (
<TextClassContext.Provider value={textClassName}>
<Pressable>
<Text>{children}</Text> {/* Gets context styling */}
</Pressable>
</TextClassContext.Provider>
);
}
```
## File Organization
```
apps/mobile/
├── app/ # Expo Router screens
│ ├── (external)/ # Public routes
│ ├── (internal)/
│ │ ├── (tabs)/ # Tab navigation
│ │ ├── (standard)/ # Stack navigation
│ │ └── record/ # Recording flow
├── components/
│ ├── ui/ # React Native Reusables
│ ├── recording/ # Recording UI
│ ├── activity/ # Activity components
│ └── shared/ # Shared components
├── lib/
│ ├── hooks/ # Custom hooks
│ │ └── useActivityRecorder.ts # 8 specialized hooks
│ ├── stores/ # Zustand stores
│ ├── services/ # Business logic
│ └── providers/ # React Context providers
└── assets/
```
## Naming Conventions
- **Components**: `PascalCase` → `ActivityCard.tsx`, `RecordingFooter.tsx`
- **Hooks**: `camelCase` with `use` prefix → `useActivityRecorder.ts`, `useFormMutation.ts`
- **Stores**: `camelCase` with `Store` suffix → `authStore.ts`, `activitySelectionStore.ts`
- **Utilities**: `camelCase` → `formatDuration.ts`
- **Constants**: `SCREAMING_SNAKE_CASE` → `MAX_HEART_RATE = 220`
## Common Scenarios
### Scenario 1: Creating a New Recording Screen Component
**Approach**:
1. Import service from provider
2. Use specific hooks for data needs
3. Style all Text elements
4. Handle loading/error states
5. Wrap with ErrorBoundary
**Example**:
```tsx
import { useSharedActivityRecorder } from "@/lib/providers/ActivityRecorderProvider";
import {
useRecordingState,
useCurrentReadings,
} from "@/lib/hooks/useActivityRecorder";
import { ErrorBoundary, ScreenErrorFallback } from "@/components/ErrorBoundary";
function RecordingMetrics() {
const service = useSharedActivityRecorder();
const state = useRecordingState(service);
const readings = useCurrentReadings(service);
if (!service) {
return <Text className="text-muted-foreground">Loading...</Text>;
}
return (
<View className="p-4 bg-card">
<Text className="text-foreground text-lg font-semibold">
{readings.heartRate ? `${readings.heartRate} bpm` : "--"}
</Text>
</View>
);
}
export default function RecordingMetricsWithErrorBoundary() {
return (
<ErrorBoundary fallback={ScreenErrorFallback}>
<RecordingMetrics />
</ErrorBoundary>
);
}
```
### Scenario 2: Form with Validation and Submission
**Approach**:
1. Use React Hook Form + Zod
2. Use useFormMutation for submission
3. Handle field errors automatically
4. Show loading state on button
**Example**:
```tsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { activitySchema } from "@repo/core/schemas";
import { useFormMutation } from "@/lib/hooks/useFormMutation";
import { trpc } from "@/lib/trpc";
function CreateActivityForm() {
const form = useForm({
resolver: zodResolver(activitySchema),
defaultValues: { name: "", type: "run", distance: 0 },
});
const mutation = useFormMutation({
mutationFn: async (data) => trpc.activities.create.mutate(data),
form,
invalidateQueries: [["activities"]],
successMessage: "Activity created!",
onSuccess: () => router.back(),
});
return (
<View className="p-4">
<Input
{...form.register("name")}
placeholder="Activity name"
className="bg-background"
/>
{form.formState.errors.name && (
<Text className="text-destructive text-sm">
{form.formState.errors.name.message}
</Text>
)}
<Button
onPress={form.handleSubmit(mutation.mutate)}
disabled={mutation.isLoading}
className="mt-4"
>
<Text className="text-primary-foreground">
{mutation.isLoading ? "Creating..." : "Create"}
</Text>
</Button>
</View>
);
}
```
## Dependencies
**Required**:
- `expo` v54+
- `expo-router` v6+
- `react-native-reusables` (shadcn-inspired components)
- `nativewind` v4
- `lucide-react-native` (icons)
- `@tanstack/react-query` v5
- `zustand` (state management)
**Optional**:
- `react-hook-form` + `@hookform/resolvers` (complex forms)
- `expo-location` (GPS tracking)
- `expo-sensors` (device sensors)
**Forbidden**:
- Never import from `@repo/supabase` directly (use tRPC)
- Never import database clients in mobile app
## Testing Requirements
- Test component rendering with React Native Testing Library
- Test hooks with renderHook from testing library
- Mock services/stores for isolated testing
- Test event handler callbacks with jest.fn()
- Test navigation with mocked router
## Checklist
Quick reference for mobile implementation:
- [ ] Every Text component has className with color
- [ ] Icons use `<Icon as={Component} />` pattern
- [ ] Service accessed via useSharedActivityRecorder
- [ ] Event subscriptions cleaned up in useEffect
- [ ] Complex navigation uses selection store
- [ ] Platform-specific styles use ios:/android: prefixes
- [ ] Forms use useFormMutation for submissions
- [ ] List items memoized with custom comparison
- [ ] Error boundaries wrap screens
- [ ] Safe area insets handled for notches
## Related Skills
- [Core Package Skill](./core-package-skill.md) - Pure function patterns
- [Backend Skill](./backend-skill.md) - tRPC integration
- [Testing Skill](./testing-skill.md) - Mobile testing patterns
## Version History
- **1.0.0** (2026-01-21): Initial version based on codebase analysis
---
**Next Review**: 2026-02-21Related Skills
modus-frontend
Build production-grade frontend interfaces using the Modus 2.0 Design System with Tailwind CSS. Use this skill when the user asks to build web components, pages, dashboards, or application screens. Enforces the 9-color semantic system, Modus Web Components, Modus Icons, and design system compliance. Framework-agnostic (works with React, Angular, or any framework).
mobile_react_native
React Native best practices, hooks, navigation ve performance optimization.
mobile-ui-development-rule
General rules pertaining to Mobile UI development. Covers UI/UX best practices, state management, and navigation patterns.
mobile-security-expert
移动安全漏洞挖掘知识库,基于HackerOne公开报告提供Android和iOS应用的漏洞挖掘手法、技术细节和代码模式分析;用于安全研究人员和漏洞挖掘者学习参考、代码审计和漏洞检测指导。
mobile-security-coder
Expert in secure mobile coding practices specializing in input validation, WebView security, and mobile-specific security patterns.
mobile-offline-support
Implement offline-first mobile apps with local storage, sync strategies, and conflict resolution. Covers AsyncStorage, Realm, SQLite, and background sync patterns.
mobile-guide
Comprehensive mobile development guide for iOS, Android, React Native, and Flutter. Includes Swift, Kotlin, and cross-platform frameworks. Use when building mobile applications, iOS, Android, or cross-platform apps.
mobile-games
Mobile game development principles. Touch input, battery, performance, app stores.
mobile-first-design-rules
Focuses on rules and best practices for mobile-first design and responsive typography using tailwind.
mobile-development
Cross-platform and native mobile development. Frameworks: React Native, Flutter, Swift/SwiftUI, Kotlin/Jetpack Compose. Capabilities: mobile UI, offline-first architecture, push notifications, deep linking, biometrics, app store deployment. Actions: build, create, implement, optimize, test, deploy mobile apps. Keywords: iOS, Android, React Native, Flutter, Swift, Kotlin, mobile app, offline sync, push notification, deep link, biometric auth, App Store, Play Store, iOS HIG, Material Design, battery optimization, memory management, mobile performance. Use when: building mobile apps, implementing mobile-first UX, choosing native vs cross-platform, optimizing battery/memory/network, deploying to app stores, handling mobile-specific features.
mobile-developer
Develop React Native, Flutter, or native mobile apps with modern architecture patterns. Masters cross-platform development, native integrations, offline sync, and app store optimization. Use PROACTIVELY for mobile features, cross-platform code, or app optimization.
mobile-design
Mobile-first design and engineering doctrine for iOS and Android apps. Covers touch interaction, performance, platform conventions, offline behavior, and mobile-specific decision-making. Teaches pr...