From react-native-hifi
Builds React Native and Expo UIs: Expo Router navigation, Flexbox layouts, animations, native controls, modals, gestures, and StyleSheet styling.
npx claudepluginhub bidah/react-native-hifi --plugin react-native-hifiThis skill uses the workspace's default tool permissions.
React Native UI differs fundamentally from web UI. There is no CSS, no DOM, no browser layout engine. Everything renders to native platform views through a bridge. Understanding this architecture prevents the most common mistakes.
Guides building native UI apps with Expo Router: fundamentals, styling, components, navigation, animations, iOS controls, tabs, and effects.
Guides building native-feeling Expo Router apps covering navigation, styling, components, animations, tabs, controls, effects, and Expo Go vs custom builds.
Builds, optimizes, and debugs React Native and Expo mobile apps including navigation hierarchies, native modules, FlatList performance with memo/useCallback, and iOS/Android platform code.
Share bugs, ideas, or general feedback.
React Native UI differs fundamentally from web UI. There is no CSS, no DOM, no browser layout engine. Everything renders to native platform views through a bridge. Understanding this architecture prevents the most common mistakes.
Core principle: Think in native components and Flexbox, not in HTML/CSS. Every visual element maps to a platform-native view.
Expo Router uses file-based routing, similar to Next.js. Files in the app/ directory automatically become routes.
app/
_layout.tsx # Root layout (wraps all routes)
index.tsx # Home screen (/)
settings.tsx # /settings
(tabs)/
_layout.tsx # Tab navigator layout
home.tsx # Tab: home
profile.tsx # Tab: profile
[id].tsx # Dynamic route: /123, /abc
(auth)/
login.tsx # Group: doesn't affect URL
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: 'Home' }} />
<Stack.Screen name="settings" options={{ headerShown: false }} />
</Stack>
);
}
import { Link, useRouter } from 'expo-router';
// Declarative
<Link href="/settings">Go to Settings</Link>
<Link href={{ pathname: '/user/[id]', params: { id: '42' } }}>User 42</Link>
// Imperative
const router = useRouter();
router.push('/settings');
router.replace('/login'); // No back button
router.back();
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() {
return (
<Tabs>
<Tabs.Screen
name="home"
options={{
title: 'Home',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
</Tabs>
);
}
React Native uses Flexbox by default. Key differences from web Flexbox:
| Property | React Native Default | Web Default |
|---|---|---|
flexDirection | column | row |
alignContent | flex-start | stretch |
| Units | Unitless (dp) | px, em, rem, etc. |
// Centering content
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
// Row with spacing
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
},
});
// Scrollable list with fixed header/footer
const styles = StyleSheet.create({
container: { flex: 1 },
header: { height: 60 },
content: { flex: 1 }, // Takes remaining space
footer: { height: 80 },
});
Always account for notches, status bars, and home indicators:
import { SafeAreaView } from 'react-native-safe-area-context';
export default function Screen() {
return (
<SafeAreaView style={{ flex: 1 }} edges={['top', 'bottom']}>
{/* Content */}
</SafeAreaView>
);
}
For performant, 60fps animations that run on the native thread.
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
} from 'react-native-reanimated';
function AnimatedBox() {
const offset = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: offset.value }],
}));
return (
<Animated.View style={[styles.box, animatedStyle]}>
<Pressable onPress={() => {
offset.value = withSpring(offset.value === 0 ? 200 : 0);
}}>
<Text>Tap me</Text>
</Pressable>
</Animated.View>
);
}
import Animated, { FadeIn, SlideOutRight } from 'react-native-reanimated';
<Animated.View entering={FadeIn.duration(500)} exiting={SlideOutRight}>
<Text>I animate in and out</Text>
</Animated.View>
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withDecay,
} from 'react-native-reanimated';
function DraggableCard() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const pan = Gesture.Pan()
.onUpdate((event) => {
translateX.value = event.translationX;
translateY.value = event.translationY;
})
.onEnd((event) => {
translateX.value = withDecay({ velocity: event.velocityX });
translateY.value = withDecay({ velocity: event.velocityY });
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
}));
return (
<GestureDetector gesture={pan}>
<Animated.View style={[styles.card, animatedStyle]} />
</GestureDetector>
);
}
Use platform-native controls that match iOS design language:
import { Switch, Slider, SegmentedControl } from '@expo/ui';
function SettingsScreen() {
const [enabled, setEnabled] = useState(false);
const [volume, setVolume] = useState(0.5);
const [tab, setTab] = useState(0);
return (
<View>
<Switch value={enabled} onValueChange={setEnabled} />
<Slider
value={volume}
onValueChange={setVolume}
minimumValue={0}
maximumValue={1}
/>
<SegmentedControl
values={['Daily', 'Weekly', 'Monthly']}
selectedIndex={tab}
onValueChange={setTab}
/>
</View>
);
}
Use Apple's SF Symbols icon set on iOS:
import { SymbolView } from 'expo-symbols';
<SymbolView
name="heart.fill"
style={{ width: 24, height: 24 }}
tintColor="red"
weight="semibold"
/>
Common symbol names: chevron.right, gear, person.fill, star.fill, bell.fill, magnifyingglass, plus.circle.fill.
// app/_layout.tsx
<Stack>
<Stack.Screen name="index" />
<Stack.Screen
name="modal"
options={{
presentation: 'modal',
headerTitle: 'Settings',
}}
/>
</Stack>
// app/modal.tsx - presented as native modal
<Stack.Screen
name="form-sheet"
options={{
presentation: 'formSheet',
sheetAllowedDetents: [0.5, 1.0], // Half and full screen
sheetGrabberVisible: true,
sheetCornerRadius: 24,
}}
/>
When you need to wrap native views for use with platform-specific APIs:
import { requireNativeView } from 'expo';
const NativeMapView = requireNativeView('MapView');
function Map({ region, onRegionChange }) {
return (
<NativeMapView
style={{ flex: 1 }}
region={region}
onRegionChange={onRegionChange}
/>
);
}
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
padding: 16,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
marginBottom: 8,
},
// Shadow (iOS)
card: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
// Shadow (Android)
elevation: 3,
borderRadius: 8,
backgroundColor: '#fff',
padding: 16,
},
});
Text within Text)margin: 16 works, but margin: '16px 8px' does notbackgroundColor not background-colorPressable with style functions for hover/press statesshadowX for iOS, elevation for Android<Pressable
style={({ pressed }) => [
styles.button,
pressed && styles.buttonPressed,
]}
>
{({ pressed }) => (
<Text style={pressed ? styles.textPressed : styles.text}>
Press me
</Text>
)}
</Pressable>
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
...Platform.select({
ios: { shadowColor: '#000', shadowOpacity: 0.1 },
android: { elevation: 3 },
}),
},
});
| Mistake | Fix |
|---|---|
Using 100vh or CSS units | Use flex: 1 or Dimensions.get('window').height |
| Forgetting SafeAreaView | Always wrap top-level screens in SafeAreaView |
Animating with setState | Use Reanimated shared values for 60fps animations |
Nesting ScrollView in ScrollView | Use FlatList with ListHeaderComponent |
Using overflow: 'hidden' expecting CSS behavior | Android clips children differently; test both platforms |
| Hardcoding dimensions | Use flex, %, or useWindowDimensions() |
Missing key prop in lists | Use FlatList with keyExtractor instead of .map() |
| Task | Solution |
|---|---|
| Navigate to screen | router.push('/path') or <Link href="/path"> |
| Tab navigation | (tabs)/_layout.tsx with <Tabs> |
| Center content | justifyContent: 'center', alignItems: 'center' |
| Spring animation | offset.value = withSpring(target) |
| Native switch | <Switch> from @expo/ui |
| Modal screen | presentation: 'modal' in Stack options |
| Platform check | Platform.OS === 'ios' |
| Safe area | <SafeAreaView edges={['top']}> |