xiaohongshu
xiaohongshu
mobile-frontend
React Native patterns, NativeWind styling, React Native Reusables components, mobile-specific patterns
Full skill instructions
When to use: Every single Text component in the app Why: React Native has no style inheritance
// ❌ 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:
text-foreground, text-muted-foreground, text-destructiveWhen to use: Any time you need an icon Why: Provides consistent styling and theming
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:
<Icon as={IconComponent} /> patternWhen to use: ActivityRecorder service across multiple screens Why: Ensures single service instance, prevents state fragmentation
// ✅ 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:
useSharedActivityRecorder() to access serviceWhen to use: Subscribing to ActivityRecorder events Why: Prevents unnecessary re-renders, optimizes performance
// ✅ 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:
useRecordingState, useCurrentReadings, useSessionStatsWhen to use: Navigating with complex objects (activity plans, activity selection) Why: URL params can't encode complex objects
// ✅ 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:
When to use: Different styling for iOS vs Android Why: Platform-specific design guidelines
// ✅ 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:
ios: and android: prefixesWhen to use: Creating/updating data with forms Why: Automatic retry, error handling, field error mapping
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:
When to use: FlatList with many items Why: Prevents unnecessary re-renders
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:
Problem: Each component gets different service, sensors not shared
// ❌ 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
}
Problem: Memory leaks from event listeners
// ❌ BAD
useEffect(() => {
service.addListener("stateChanged", handleStateChange);
// Missing cleanup!
}, [service]);
// ✅ CORRECT
useEffect(() => {
const subscription = service.addListener("stateChanged", handleStateChange);
return () => subscription.remove(); // Always clean up
}, [service]);
Problem: Component re-renders on every event
// ❌ 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
Problem: Text inside custom button has no styling
// ❌ 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>
);
}
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/
PascalCase → ActivityCard.tsx, RecordingFooter.tsxcamelCase with use prefix → useActivityRecorder.ts, useFormMutation.tscamelCase with Store suffix → authStore.ts, activitySelectionStore.tscamelCase → formatDuration.tsSCREAMING_SNAKE_CASE → MAX_HEART_RATE = 220Approach:
Example:
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>
);
}
Approach:
Example:
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>
);
}
Required:
expo v54+expo-router v6+react-native-reusables (shadcn-inspired components)nativewind v4lucide-react-native (icons)@tanstack/react-query v5zustand (state management)Optional:
react-hook-form + @hookform/resolvers (complex forms)expo-location (GPS tracking)expo-sensors (device sensors)Forbidden:
@repo/supabase directly (use tRPC)Quick reference for mobile implementation:
<Icon as={Component} /> patternNext Review: 2026-02-21
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience