format: apply repo prettier (3.8.1) to the folded skills tree

963 markdown files reformatted with the repository's pinned prettier so
pnpm format:check covers the folded tree like every other repo file.

The formatter's embedded-language pass also normalized code fences
(TS semicolons, closed HTML tags in examples, lowercased CSS hex colors,
one renumbered list that skipped an index). Alphanumeric token deltas vs
the fold commit were audited file-by-file; all are formatter-equivalent
markup normalizations plus the four sanitized skills.
This commit is contained in:
fargo
2026-08-19 14:37:17 -05:00
parent d2eeb64433
commit 1a822493ba
962 changed files with 29594 additions and 27188 deletions
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,6 @@
---
name: vercel-react-native-skills
description:
React Native and Expo best practices for building performant mobile apps. Use
description: React Native and Expo best practices for building performant mobile apps. Use
when building React Native components, optimizing list performance,
implementing animations, or working with native modules. Triggers on tasks
involving React Native, Expo, mobile performance, or native platform APIs.
@@ -15,14 +15,14 @@ Brief explanation of the rule and why it matters. This should be clear and conci
```typescript
// Bad code example here
const bad = example()
const bad = example();
```
**Correct (description of what's right):**
```typescript
// Good code example here
const good = example()
const good = example();
```
Reference: [Link to documentation or resource](https://example.com)
@@ -15,18 +15,18 @@ for side effects, not derivations.
**Incorrect (useAnimatedReaction for derivation):**
```tsx
import { useSharedValue, useAnimatedReaction } from 'react-native-reanimated'
import { useSharedValue, useAnimatedReaction } from 'react-native-reanimated';
function MyComponent() {
const progress = useSharedValue(0)
const opacity = useSharedValue(1)
const progress = useSharedValue(0);
const opacity = useSharedValue(1);
useAnimatedReaction(
() => progress.value,
(current) => {
opacity.value = 1 - current
}
)
opacity.value = 1 - current;
},
);
// ...
}
@@ -35,12 +35,12 @@ function MyComponent() {
**Correct (useDerivedValue):**
```tsx
import { useSharedValue, useDerivedValue } from 'react-native-reanimated'
import { useSharedValue, useDerivedValue } from 'react-native-reanimated';
function MyComponent() {
const progress = useSharedValue(0)
const progress = useSharedValue(0);
const opacity = useDerivedValue(() => 1 - progress.get())
const opacity = useDerivedValue(() => 1 - progress.get());
// ...
}
@@ -15,19 +15,15 @@ JS thread round-trip for press animations.
**Incorrect (Pressable with JS thread callbacks):**
```tsx
import { Pressable } from 'react-native'
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
} from 'react-native-reanimated'
import { Pressable } from 'react-native';
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated';
function AnimatedButton({ onPress }: { onPress: () => void }) {
const scale = useSharedValue(1)
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}))
}));
return (
<Pressable
@@ -39,43 +35,41 @@ function AnimatedButton({ onPress }: { onPress: () => void }) {
<Text>Press me</Text>
</Animated.View>
</Pressable>
)
);
}
```
**Correct (GestureDetector with UI thread worklets):**
```tsx
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
interpolate,
runOnJS,
} from 'react-native-reanimated'
} from 'react-native-reanimated';
function AnimatedButton({ onPress }: { onPress: () => void }) {
// Store the press STATE (0 = not pressed, 1 = pressed)
const pressed = useSharedValue(0)
const pressed = useSharedValue(0);
const tap = Gesture.Tap()
.onBegin(() => {
pressed.set(withTiming(1))
pressed.set(withTiming(1));
})
.onFinalize(() => {
pressed.set(withTiming(0))
pressed.set(withTiming(0));
})
.onEnd(() => {
runOnJS(onPress)()
})
runOnJS(onPress)();
});
// Derive visual values from the state
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ scale: interpolate(withTiming(pressed.get()), [0, 1], [1, 0.95]) },
],
}))
transform: [{ scale: interpolate(withTiming(pressed.get()), [0, 1], [1, 0.95]) }],
}));
return (
<GestureDetector gesture={tap}>
@@ -83,7 +77,7 @@ function AnimatedButton({ onPress }: { onPress: () => void }) {
<Text>Press me</Text>
</Animated.View>
</GestureDetector>
)
);
}
```
@@ -12,53 +12,49 @@ Avoid animating `width`, `height`, `top`, `left`, `margin`, or `padding`. These
**Incorrect (animates height, triggers layout every frame):**
```tsx
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated';
function CollapsiblePanel({ expanded }: { expanded: boolean }) {
const animatedStyle = useAnimatedStyle(() => ({
height: withTiming(expanded ? 200 : 0), // triggers layout on every frame
overflow: 'hidden',
}))
}));
return <Animated.View style={animatedStyle}>{children}</Animated.View>
return <Animated.View style={animatedStyle}>{children}</Animated.View>;
}
```
**Correct (animates scaleY, GPU-accelerated):**
```tsx
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated';
function CollapsiblePanel({ expanded }: { expanded: boolean }) {
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ scaleY: withTiming(expanded ? 1 : 0) },
],
transform: [{ scaleY: withTiming(expanded ? 1 : 0) }],
opacity: withTiming(expanded ? 1 : 0),
}))
}));
return (
<Animated.View style={[{ height: 200, transformOrigin: 'top' }, animatedStyle]}>
{children}
</Animated.View>
)
);
}
```
**Correct (animates translateY for slide animations):**
```tsx
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated';
function SlideIn({ visible }: { visible: boolean }) {
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateY: withTiming(visible ? 0 : 100) },
],
transform: [{ translateY: withTiming(visible ? 0 : 100) }],
opacity: withTiming(visible ? 1 : 0),
}))
}));
return <Animated.View style={animatedStyle}>{children}</Animated.View>
return <Animated.View style={animatedStyle}>{children}</Animated.View>;
}
```
@@ -13,23 +13,23 @@ Use the `expo-font` config plugin to embed fonts at build time instead of
**Incorrect (async font loading):**
```tsx
import { useFonts } from 'expo-font'
import { Text, View } from 'react-native'
import { useFonts } from 'expo-font';
import { Text, View } from 'react-native';
function App() {
const [fontsLoaded] = useFonts({
'Geist-Bold': require('./assets/fonts/Geist-Bold.otf'),
})
});
if (!fontsLoaded) {
return null
return null;
}
return (
<View>
<Text style={{ fontFamily: 'Geist-Bold' }}>Hello</Text>
</View>
)
);
}
```
@@ -52,7 +52,7 @@ function App() {
```
```tsx
import { Text, View } from 'react-native'
import { Text, View } from 'react-native';
function App() {
// No loading state needed—font is already available
@@ -60,7 +60,7 @@ function App() {
<View>
<Text style={{ fontFamily: 'Geist-Bold' }}>Hello</Text>
</View>
)
);
}
```
@@ -13,8 +13,8 @@ not directly from packages. This enables global changes and easy refactoring.
**Incorrect (imports directly from package):**
```tsx
import { View, Text } from 'react-native'
import { Button } from '@ui/button'
import { View, Text } from 'react-native';
import { Button } from '@ui/button';
function Profile() {
return (
@@ -22,7 +22,7 @@ function Profile() {
<Text>Hello</Text>
<Button>Save</Button>
</View>
)
);
}
```
@@ -30,30 +30,28 @@ function Profile() {
```tsx
// components/view.tsx
import { View as RNView } from 'react-native'
import { View as RNView } from 'react-native';
// ideal: pick the props you will actually use to control implementation
export function View(
props: Pick<React.ComponentProps<typeof RNView>, 'style' | 'children'>
) {
return <RNView {...props} />
export function View(props: Pick<React.ComponentProps<typeof RNView>, 'style' | 'children'>) {
return <RNView {...props} />;
}
```
```tsx
// components/text.tsx
export { Text } from 'react-native'
export { Text } from 'react-native';
```
```tsx
// components/button.tsx
export { Button } from '@ui/button'
export { Button } from '@ui/button';
```
```tsx
import { View } from '@/components/view'
import { Text } from '@/components/text'
import { Button } from '@/components/button'
import { View } from '@/components/view';
import { Text } from '@/components/text';
import { Button } from '@/components/button';
function Profile() {
return (
@@ -61,7 +59,7 @@ function Profile() {
<Text>Hello</Text>
<Button>Save</Button>
</View>
)
);
}
```
@@ -18,8 +18,8 @@ function Price({ amount }: { amount: number }) {
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
})
return <Text>{formatter.format(amount)}</Text>
});
return <Text>{formatter.format(amount)}</Text>;
}
```
@@ -29,10 +29,10 @@ function Price({ amount }: { amount: number }) {
const currencyFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
})
});
function Price({ amount }: { amount: number }) {
return <Text>{currencyFormatter.format(amount)}</Text>
return <Text>{currencyFormatter.format(amount)}</Text>;
}
```
@@ -41,20 +41,20 @@ function Price({ amount }: { amount: number }) {
```tsx
const dateFormatter = useMemo(
() => new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }),
[locale]
)
[locale],
);
```
**Common formatters to hoist:**
```tsx
// Module-level formatters
const dateFormatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' })
const timeFormatter = new Intl.DateTimeFormat('en-US', { timeStyle: 'short' })
const percentFormatter = new Intl.NumberFormat('en-US', { style: 'percent' })
const dateFormatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' });
const timeFormatter = new Intl.DateTimeFormat('en-US', { timeStyle: 'short' });
const percentFormatter = new Intl.NumberFormat('en-US', { style: 'percent' });
const relativeFormatter = new Intl.RelativeTimeFormat('en-US', {
numeric: 'auto',
})
});
```
Creating `Intl` objects is significantly more expensive than `RegExp` or plain
@@ -18,15 +18,15 @@ Where needed, use context selectors within list items.
```tsx
function DomainSearch() {
const { keyword, setKeyword } = useKeywordZustandState()
const { data: tlds } = useTlds()
const { keyword, setKeyword } = useKeywordZustandState();
const { data: tlds } = useTlds();
// Bad: creates new objects on every render, reparenting the entire list on every keystroke
const domains = tlds.map((tld) => ({
domain: `${keyword}.${tld.name}`,
tld: tld.name,
price: tld.price,
}))
}));
return (
<>
@@ -36,17 +36,17 @@ function DomainSearch() {
renderItem={({ item }) => <DomainItem item={item} keyword={keyword} />}
/>
</>
)
);
}
```
**Correct (stable references, transform inside items):**
```tsx
const renderItem = ({ item }) => <DomainItem tld={item} />
const renderItem = ({ item }) => <DomainItem tld={item} />;
function DomainSearch() {
const { data: tlds } = useTlds()
const { data: tlds } = useTlds();
return (
<LegendList
@@ -54,14 +54,14 @@ function DomainSearch() {
data={tlds}
renderItem={renderItem}
/>
)
);
}
function DomainItem({ tld }: { tld: Tld }) {
// good: transform within items, and don't pass the dynamic data as a prop
// good: use a selector function from zustand to receive a stable string back
const domain = useKeywordZustandState((s) => s.keyword + '.' + tld.name)
return <Text>{domain}</Text>
const domain = useKeywordZustandState((s) => s.keyword + '.' + tld.name);
return <Text>{domain}</Text>;
}
```
@@ -73,9 +73,9 @@ references are stable. For instance, if you sort a list of objects:
```tsx
// good: creates a new array instance without mutating the inner objects
// good: parent array reference is unaffected by typing and updating "keyword"
const sortedTlds = tlds.toSorted((a, b) => a.name.localeCompare(b.name))
const sortedTlds = tlds.toSorted((a, b) => a.name.localeCompare(b.name));
return <LegendList data={sortedTlds} renderItem={renderItem} />
return <LegendList data={sortedTlds} renderItem={renderItem} />;
```
Even though this creates a new array instance `sortedTlds`, the inner object
@@ -84,10 +84,10 @@ references are stable.
**With zustand for dynamic data (avoids parent re-renders):**
```tsx
const useSearchStore = create<{ keyword: string }>(() => ({ keyword: '' }))
const useSearchStore = create<{ keyword: string }>(() => ({ keyword: '' }));
function DomainSearch() {
const { data: tlds } = useTlds()
const { data: tlds } = useTlds();
return (
<>
@@ -98,14 +98,14 @@ function DomainSearch() {
renderItem={({ item }) => <DomainItem tld={item} />}
/>
</>
)
);
}
function DomainItem({ tld }: { tld: Tld }) {
// Select only what you need—component only re-renders when keyword changes
const keyword = useSearchStore((s) => s.keyword)
const domain = `${keyword}.${tld.name}`
return <Text>{domain}</Text>
const keyword = useSearchStore((s) => s.keyword);
const domain = `${keyword}.${tld.name}`;
return <Text>{domain}</Text>;
}
```
@@ -122,8 +122,8 @@ is in charge of accessing the state rather than the parent:
```tsx
function DomainItemFavoriteButton({ tld }: { tld: Tld }) {
const isFavorited = useFavoritesStore((s) => s.favorites.has(tld.id))
return <TldFavoriteButton isFavorited={isFavorited} />
const isFavorited = useFavoritesStore((s) => s.favorites.has(tld.id));
return <TldFavoriteButton isFavorited={isFavorited} />;
}
```
@@ -18,13 +18,10 @@ function ProductItem({ product }: { product: Product }) {
return (
<View>
{/* 4000x3000 image loaded for a 100x100 thumbnail */}
<Image
source={{ uri: product.imageUrl }}
style={{ width: 100, height: 100 }}
/>
<Image source={{ uri: product.imageUrl }} style={{ width: 100, height: 100 }} />
<Text>{product.name}</Text>
</View>
)
);
}
```
@@ -33,18 +30,18 @@ function ProductItem({ product }: { product: Product }) {
```tsx
function ProductItem({ product }: { product: Product }) {
// Request a 200x200 image (2x for retina)
const thumbnailUrl = `${product.imageUrl}?w=200&h=200&fit=cover`
const thumbnailUrl = `${product.imageUrl}?w=200&h=200&fit=cover`;
return (
<View>
<Image
source={{ uri: thumbnailUrl }}
style={{ width: 100, height: 100 }}
contentFit='cover'
contentFit="cover"
/>
<Text>{product.name}</Text>
</View>
)
);
}
```
@@ -25,7 +25,7 @@ function UserList({ users }: { users: User[] }) {
/>
)}
/>
)
);
}
```
@@ -53,7 +53,7 @@ function UserList({ users }: { users: User[] }) {
<UserRow user={item} />
)}
/>
)
);
}
```
@@ -16,18 +16,15 @@ during scroll—expensive items cause jank.
```tsx
function ProductRow({ id }: { id: string }) {
// Bad: query inside list item
const { data: product } = useQuery(['product', id], () => fetchProduct(id))
const { data: product } = useQuery(['product', id], () => fetchProduct(id));
// Bad: multiple context accesses
const theme = useContext(ThemeContext)
const user = useContext(UserContext)
const cart = useContext(CartContext)
const theme = useContext(ThemeContext);
const user = useContext(UserContext);
const cart = useContext(CartContext);
// Bad: expensive computation
const recommendations = useMemo(
() => computeRecommendations(product),
[product]
)
const recommendations = useMemo(() => computeRecommendations(product), [product]);
return <View>{/* ... */}</View>
return <View>{/* ... */}</View>;
}
```
@@ -42,7 +39,7 @@ function ProductRow({ name, price, imageUrl }: Props) {
<Text>{name}</Text>
<Text>{price}</Text>
</View>
)
);
}
```
@@ -51,7 +48,7 @@ function ProductRow({ name, price, imageUrl }: Props) {
```tsx
// Parent fetches all data once
function ProductList() {
const { data: products } = useQuery(['products'], fetchProducts)
const { data: products } = useQuery(['products'], fetchProducts);
return (
<LegendList
@@ -60,7 +57,7 @@ function ProductList() {
<ProductRow name={item.name} price={item.price} imageUrl={item.image} />
)}
/>
)
);
}
```
@@ -69,15 +66,15 @@ function ProductList() {
```tsx
// Incorrect: Context causes re-render when any cart value changes
function ProductRow({ id, name }: Props) {
const { items } = useContext(CartContext)
const inCart = items.includes(id)
const { items } = useContext(CartContext);
const inCart = items.includes(id);
// ...
}
// Correct: Zustand selector only re-renders when this specific value changes
function ProductRow({ id, name }: Props) {
// use Set.has (created once at the root) instead of Array.includes()
const inCart = useCartStore((s) => s.items.has(id))
const inCart = useCartStore((s) => s.items.has(id));
// ...
}
```
@@ -15,36 +15,32 @@ image component.
**Incorrect (single component with conditionals):**
```tsx
type Item = { id: string; text?: string; imageUrl?: string; isHeader?: boolean }
type Item = { id: string; text?: string; imageUrl?: string; isHeader?: boolean };
function ListItem({ item }: { item: Item }) {
if (item.isHeader) {
return <HeaderItem title={item.text} />
return <HeaderItem title={item.text} />;
}
if (item.imageUrl) {
return <ImageItem url={item.imageUrl} />
return <ImageItem url={item.imageUrl} />;
}
return <MessageItem text={item.text} />
return <MessageItem text={item.text} />;
}
function Feed({ items }: { items: Item[] }) {
return (
<LegendList
data={items}
renderItem={({ item }) => <ListItem item={item} />}
recycleItems
/>
)
<LegendList data={items} renderItem={({ item }) => <ListItem item={item} />} recycleItems />
);
}
```
**Correct (typed items with separate components):**
```tsx
type HeaderItem = { id: string; type: 'header'; title: string }
type MessageItem = { id: string; type: 'message'; text: string }
type ImageItem = { id: string; type: 'image'; url: string }
type FeedItem = HeaderItem | MessageItem | ImageItem
type HeaderItem = { id: string; type: 'header'; title: string };
type MessageItem = { id: string; type: 'message'; text: string };
type ImageItem = { id: string; type: 'image'; url: string };
type FeedItem = HeaderItem | MessageItem | ImageItem;
function Feed({ items }: { items: FeedItem[] }) {
return (
@@ -55,16 +51,16 @@ function Feed({ items }: { items: FeedItem[] }) {
renderItem={({ item }) => {
switch (item.type) {
case 'header':
return <SectionHeader title={item.title} />
return <SectionHeader title={item.title} />;
case 'message':
return <MessageRow text={item.text} />
return <MessageRow text={item.text} />;
case 'image':
return <ImageRow url={item.url} />
return <ImageRow url={item.url} />;
}
}}
recycleItems
/>
)
);
}
```
@@ -84,13 +80,13 @@ function Feed({ items }: { items: FeedItem[] }) {
getEstimatedItemSize={(index, item, itemType) => {
switch (itemType) {
case 'header':
return 48
return 48;
case 'message':
return 72
return 72;
case 'image':
return 300
return 300;
default:
return 72
return 72;
}
}}
renderItem={({ item }) => {
@@ -22,7 +22,7 @@ function Feed({ items }: { items: Item[] }) {
<ItemCard key={item.id} item={item} />
))}
</ScrollView>
)
);
}
// 50 items = 50 components mounted, even if only 10 visible
```
@@ -30,7 +30,7 @@ function Feed({ items }: { items: Item[] }) {
**Correct (virtualizer renders only visible items):**
```tsx
import { LegendList } from '@legendapp/list'
import { LegendList } from '@legendapp/list';
function Feed({ items }: { items: Item[] }) {
return (
@@ -41,7 +41,7 @@ function Feed({ items }: { items: Item[] }) {
keyExtractor={(item) => item.id}
estimatedItemSize={80}
/>
)
);
}
// Only ~10-15 visible items mounted at a time
```
@@ -49,7 +49,7 @@ function Feed({ items }: { items: Item[] }) {
**Alternative (FlashList):**
```tsx
import { FlashList } from '@shopify/flash-list'
import { FlashList } from '@shopify/flash-list';
function Feed({ items }: { items: Item[] }) {
return (
@@ -59,7 +59,7 @@ function Feed({ items }: { items: Item[] }) {
renderItem={({ item }) => <ItemCard item={item} />}
keyExtractor={(item) => item.id}
/>
)
);
}
```
@@ -22,34 +22,34 @@ tabs. Avoid `@react-navigation/bottom-tabs` when native feel matters.
**Incorrect (JS stack navigator):**
```tsx
import { createStackNavigator } from '@react-navigation/stack'
import { createStackNavigator } from '@react-navigation/stack';
const Stack = createStackNavigator()
const Stack = createStackNavigator();
function App() {
return (
<Stack.Navigator>
<Stack.Screen name='Home' component={HomeScreen} />
<Stack.Screen name='Details' component={DetailsScreen} />
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
)
);
}
```
**Correct (native stack with react-navigation):**
```tsx
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator()
const Stack = createNativeStackNavigator();
function App() {
return (
<Stack.Navigator>
<Stack.Screen name='Home' component={HomeScreen} />
<Stack.Screen name='Details' component={DetailsScreen} />
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
)
);
}
```
@@ -57,10 +57,10 @@ function App() {
```tsx
// app/_layout.tsx
import { Stack } from 'expo-router'
import { Stack } from 'expo-router';
export default function Layout() {
return <Stack />
return <Stack />;
}
```
@@ -69,46 +69,46 @@ export default function Layout() {
**Incorrect (JS bottom tabs):**
```tsx
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const Tab = createBottomTabNavigator()
const Tab = createBottomTabNavigator();
function App() {
return (
<Tab.Navigator>
<Tab.Screen name='Home' component={HomeScreen} />
<Tab.Screen name='Settings' component={SettingsScreen} />
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Settings" component={SettingsScreen} />
</Tab.Navigator>
)
);
}
```
**Correct (native bottom tabs with react-navigation):**
```tsx
import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation'
import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation';
const Tab = createNativeBottomTabNavigator()
const Tab = createNativeBottomTabNavigator();
function App() {
return (
<Tab.Navigator>
<Tab.Screen
name='Home'
name="Home"
component={HomeScreen}
options={{
tabBarIcon: () => ({ sfSymbol: 'house' }),
}}
/>
<Tab.Screen
name='Settings'
name="Settings"
component={SettingsScreen}
options={{
tabBarIcon: () => ({ sfSymbol: 'gear' }),
}}
/>
</Tab.Navigator>
)
);
}
```
@@ -116,21 +116,21 @@ function App() {
```tsx
// app/(tabs)/_layout.tsx
import { NativeTabs } from 'expo-router/unstable-native-tabs'
import { NativeTabs } from 'expo-router/unstable-native-tabs';
export default function TabLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name='index'>
<NativeTabs.Trigger name="index">
<NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Icon sf='house.fill' md='home' />
<NativeTabs.Trigger.Icon sf="house.fill" md="home" />
</NativeTabs.Trigger>
<NativeTabs.Trigger name='settings'>
<NativeTabs.Trigger name="settings">
<NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Icon sf='gear' md='settings' />
<NativeTabs.Trigger.Icon sf="gear" md="settings" />
</NativeTabs.Trigger>
</NativeTabs>
)
);
}
```
@@ -145,10 +145,10 @@ behind the translucent tab bar. If you need to disable this, use
```tsx
<Stack.Screen
name='Profile'
name="Profile"
component={ProfileScreen}
options={{
header: () => <CustomHeader title='Profile' />,
header: () => <CustomHeader title="Profile" />,
}}
/>
```
@@ -157,7 +157,7 @@ behind the translucent tab bar. If you need to disable this, use
```tsx
<Stack.Screen
name='Profile'
name="Profile"
component={ProfileScreen}
options={{
title: 'Profile',
@@ -16,35 +16,35 @@ creates new references and breaks memoization.
**Incorrect (dotting into object):**
```tsx
import { useRouter } from 'expo-router'
import { useRouter } from 'expo-router';
function SaveButton(props) {
const router = useRouter()
const router = useRouter();
// bad: react-compiler will key the cache on "props" and "router", which are objects that change each render
const handlePress = () => {
props.onSave()
router.push('/success') // unstable reference
}
props.onSave();
router.push('/success'); // unstable reference
};
return <Button onPress={handlePress}>Save</Button>
return <Button onPress={handlePress}>Save</Button>;
}
```
**Correct (destructure early):**
```tsx
import { useRouter } from 'expo-router'
import { useRouter } from 'expo-router';
function SaveButton({ onSave }) {
const { push } = useRouter()
const { push } = useRouter();
// good: react-compiler will key on push and onSave
const handlePress = () => {
onSave()
push('/success') // stable reference
}
onSave();
push('/success'); // stable reference
};
return <Button onPress={handlePress}>Save</Button>
return <Button onPress={handlePress}>Save</Button>;
}
```
@@ -14,32 +14,32 @@ property access—explicit methods ensure correct behavior.
**Incorrect (breaks with React Compiler):**
```tsx
import { useSharedValue } from 'react-native-reanimated'
import { useSharedValue } from 'react-native-reanimated';
function Counter() {
const count = useSharedValue(0)
const count = useSharedValue(0);
const increment = () => {
count.value = count.value + 1 // opts out of react compiler
}
count.value = count.value + 1; // opts out of react compiler
};
return <Button onPress={increment} title={`Count: ${count.value}`} />
return <Button onPress={increment} title={`Count: ${count.value}`} />;
}
```
**Correct (React Compiler compatible):**
```tsx
import { useSharedValue } from 'react-native-reanimated'
import { useSharedValue } from 'react-native-reanimated';
function Counter() {
const count = useSharedValue(0)
const count = useSharedValue(0);
const increment = () => {
count.set(count.get() + 1)
}
count.set(count.get() + 1);
};
return <Button onPress={increment} title={`Count: ${count.get()}`} />
return <Button onPress={increment} title={`Count: ${count.get()}`} />;
}
```
@@ -15,29 +15,29 @@ latest value.
**Incorrect (reads state directly):**
```tsx
const [size, setSize] = useState<Size | undefined>(undefined)
const [size, setSize] = useState<Size | undefined>(undefined);
const onLayout = (e: LayoutChangeEvent) => {
const { width, height } = e.nativeEvent.layout
const { width, height } = e.nativeEvent.layout;
// size may be stale in this closure
if (size?.width !== width || size?.height !== height) {
setSize({ width, height })
setSize({ width, height });
}
}
};
```
**Correct (dispatch updater):**
```tsx
const [size, setSize] = useState<Size | undefined>(undefined)
const [size, setSize] = useState<Size | undefined>(undefined);
const onLayout = (e: LayoutChangeEvent) => {
const { width, height } = e.nativeEvent.layout
const { width, height } = e.nativeEvent.layout;
setSize((prev) => {
if (prev?.width === width && prev?.height === height) return prev
return { width, height }
})
}
if (prev?.width === width && prev?.height === height) return prev;
return { width, height };
});
};
```
Returning the previous value from the updater skips the re-render.
@@ -48,23 +48,23 @@ re-render.
**Incorrect (unnecessary comparison for primitive state):**
```tsx
const [size, setSize] = useState<Size | undefined>(undefined)
const [size, setSize] = useState<Size | undefined>(undefined);
const onLayout = (e: LayoutChangeEvent) => {
const { width, height } = e.nativeEvent.layout
setSize((prev) => (prev === width ? prev : width))
}
const { width, height } = e.nativeEvent.layout;
setSize((prev) => (prev === width ? prev : width));
};
```
**Correct (sets primitive state directly):**
```tsx
const [size, setSize] = useState<Size | undefined>(undefined)
const [size, setSize] = useState<Size | undefined>(undefined);
const onLayout = (e: LayoutChangeEvent) => {
const { width, height } = e.nativeEvent.layout
setSize(width)
}
const { width, height } = e.nativeEvent.layout;
setSize(width);
};
```
However, if the next state depends on the current state, you should still use a
@@ -73,19 +73,19 @@ dispatch updater.
**Incorrect (reads state directly from the callback):**
```tsx
const [count, setCount] = useState(0)
const [count, setCount] = useState(0);
const onTap = () => {
setCount(count + 1)
}
setCount(count + 1);
};
```
**Correct (dispatch updater):**
```tsx
const [count, setCount] = useState(0)
const [count, setCount] = useState(0);
const onTap = () => {
setCount((prev) => prev + 1)
}
setCount((prev) => prev + 1);
};
```
@@ -15,30 +15,30 @@ source changes, not just on initial render.
**Incorrect (syncs state, loses reactivity):**
```tsx
type Props = { fallbackEnabled: boolean }
type Props = { fallbackEnabled: boolean };
function Toggle({ fallbackEnabled }: Props) {
const [enabled, setEnabled] = useState(defaultEnabled)
const [enabled, setEnabled] = useState(defaultEnabled);
// If fallbackEnabled changes, state is stale
// State mixes user intent with default value
return <Switch value={enabled} onValueChange={setEnabled} />
return <Switch value={enabled} onValueChange={setEnabled} />;
}
```
**Correct (state is user intent, reactive fallback):**
```tsx
type Props = { fallbackEnabled: boolean }
type Props = { fallbackEnabled: boolean };
function Toggle({ fallbackEnabled }: Props) {
const [_enabled, setEnabled] = useState<boolean | undefined>(undefined)
const enabled = _enabled ?? defaultEnabled
const [_enabled, setEnabled] = useState<boolean | undefined>(undefined);
const enabled = _enabled ?? defaultEnabled;
// undefined = user hasn't touched it, falls back to prop
// If defaultEnabled changes, component reflects it
// Once user interacts, their choice persists
return <Switch value={enabled} onValueChange={setEnabled} />
return <Switch value={enabled} onValueChange={setEnabled} />;
}
```
@@ -46,11 +46,11 @@ function Toggle({ fallbackEnabled }: Props) {
```tsx
function ProfileForm({ data }: { data: User }) {
const [_theme, setTheme] = useState<string | undefined>(undefined)
const theme = _theme ?? data.theme
const [_theme, setTheme] = useState<string | undefined>(undefined);
const theme = _theme ?? data.theme;
// Shows server value until user overrides
// Server refetch updates the fallback automatically
return <ThemePicker value={theme} onChange={setTheme} />
return <ThemePicker value={theme} onChange={setTheme} />;
}
```
@@ -13,20 +13,20 @@ Use the fewest state variables possible. If a value can be computed from existin
```tsx
function Cart({ items }: { items: Item[] }) {
const [total, setTotal] = useState(0)
const [itemCount, setItemCount] = useState(0)
const [total, setTotal] = useState(0);
const [itemCount, setItemCount] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, item) => sum + item.price, 0))
setItemCount(items.length)
}, [items])
setTotal(items.reduce((sum, item) => sum + item.price, 0));
setItemCount(items.length);
}, [items]);
return (
<View>
<Text>{itemCount} items</Text>
<Text>Total: ${total}</Text>
</View>
)
);
}
```
@@ -34,15 +34,15 @@ function Cart({ items }: { items: Item[] }) {
```tsx
function Cart({ items }: { items: Item[] }) {
const total = items.reduce((sum, item) => sum + item.price, 0)
const itemCount = items.length
const total = items.reduce((sum, item) => sum + item.price, 0);
const itemCount = items.length;
return (
<View>
<Text>{itemCount} items</Text>
<Text>Total: ${total}</Text>
</View>
)
);
}
```
@@ -50,14 +50,14 @@ function Cart({ items }: { items: Item[] }) {
```tsx
// Incorrect: storing both firstName, lastName, AND fullName
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [fullName, setFullName] = useState('')
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
// Correct: derive fullName
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const fullName = `${firstName} ${lastName}`
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`;
```
State should be the minimal source of truth. Everything else is derived.
@@ -20,7 +20,7 @@ function Profile({ name, count }: { name: string; count: number }) {
{name && <Text>{name}</Text>}
{count && <Text>{count} items</Text>}
</View>
)
);
}
// If name="" or count=0, renders the falsy value → crash
```
@@ -34,7 +34,7 @@ function Profile({ name, count }: { name: string; count: number }) {
{name ? <Text>{name}</Text> : null}
{count ? <Text>{count} items</Text> : null}
</View>
)
);
}
```
@@ -47,7 +47,7 @@ function Profile({ name, count }: { name: string; count: number }) {
{!!name && <Text>{name}</Text>}
{!!count && <Text>{count} items</Text>}
</View>
)
);
}
```
@@ -55,14 +55,14 @@ function Profile({ name, count }: { name: string; count: number }) {
```tsx
function Profile({ name, count }: { name: string; count: number }) {
if (!name) return null
if (!name) return null;
return (
<View>
<Text>{name}</Text>
{count > 0 ? <Text>{count} items</Text> : null}
</View>
)
);
}
```
@@ -13,10 +13,10 @@ direct child of `<View>`.
**Incorrect (crashes):**
```tsx
import { View } from 'react-native'
import { View } from 'react-native';
function Greeting({ name }: { name: string }) {
return <View>Hello, {name}!</View>
return <View>Hello, {name}!</View>;
}
// Error: Text strings must be rendered within a <Text> component.
```
@@ -24,13 +24,13 @@ function Greeting({ name }: { name: string }) {
**Correct:**
```tsx
import { View, Text } from 'react-native'
import { View, Text } from 'react-native';
function Greeting({ name }: { name: string }) {
return (
<View>
<Text>Hello, {name}!</Text>
</View>
)
);
}
```
@@ -14,40 +14,33 @@ for animations or a ref for non-reactive tracking.
**Incorrect (useState causes jank):**
```tsx
import { useState } from 'react'
import {
ScrollView,
NativeSyntheticEvent,
NativeScrollEvent,
} from 'react-native'
import { useState } from 'react';
import { ScrollView, NativeSyntheticEvent, NativeScrollEvent } from 'react-native';
function Feed() {
const [scrollY, setScrollY] = useState(0)
const [scrollY, setScrollY] = useState(0);
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
setScrollY(e.nativeEvent.contentOffset.y) // re-renders on every frame
}
setScrollY(e.nativeEvent.contentOffset.y); // re-renders on every frame
};
return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />
return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />;
}
```
**Correct (Reanimated for animations):**
```tsx
import Animated, {
useSharedValue,
useAnimatedScrollHandler,
} from 'react-native-reanimated'
import Animated, { useSharedValue, useAnimatedScrollHandler } from 'react-native-reanimated';
function Feed() {
const scrollY = useSharedValue(0)
const scrollY = useSharedValue(0);
const onScroll = useAnimatedScrollHandler({
onScroll: (e) => {
scrollY.value = e.contentOffset.y // runs on UI thread, no re-render
scrollY.value = e.contentOffset.y; // runs on UI thread, no re-render
},
})
});
return (
<Animated.ScrollView
@@ -56,27 +49,23 @@ function Feed() {
// unset this if you need higher precision over performance.
scrollEventThrottle={16}
/>
)
);
}
```
**Correct (ref for non-reactive tracking):**
```tsx
import { useRef } from 'react'
import {
ScrollView,
NativeSyntheticEvent,
NativeScrollEvent,
} from 'react-native'
import { useRef } from 'react';
import { ScrollView, NativeSyntheticEvent, NativeScrollEvent } from 'react-native';
function Feed() {
const scrollY = useRef(0)
const scrollY = useRef(0);
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
scrollY.current = e.nativeEvent.contentOffset.y // no re-render
}
scrollY.current = e.nativeEvent.contentOffset.y; // no re-render
};
return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />
return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />;
}
```
@@ -15,37 +15,37 @@ visual values from state using computation or interpolation.
**Incorrect (storing the visual output):**
```tsx
const scale = useSharedValue(1)
const scale = useSharedValue(1);
const tap = Gesture.Tap()
.onBegin(() => {
scale.set(withTiming(0.95))
scale.set(withTiming(0.95));
})
.onFinalize(() => {
scale.set(withTiming(1))
})
scale.set(withTiming(1));
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.get() }],
}))
}));
```
**Correct (storing the state, deriving the visual):**
```tsx
const pressed = useSharedValue(0) // 0 = not pressed, 1 = pressed
const pressed = useSharedValue(0); // 0 = not pressed, 1 = pressed
const tap = Gesture.Tap()
.onBegin(() => {
pressed.set(withTiming(1))
pressed.set(withTiming(1));
})
.onFinalize(() => {
pressed.set(withTiming(0))
})
pressed.set(withTiming(0));
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: interpolate(pressed.get(), [0, 1], [1, 0.95]) }],
}))
}));
```
**Why this matters:**
@@ -65,16 +65,16 @@ result.
```tsx
// Incorrect: storing derived values
const [isExpanded, setIsExpanded] = useState(false)
const [height, setHeight] = useState(0)
const [isExpanded, setIsExpanded] = useState(false);
const [height, setHeight] = useState(0);
useEffect(() => {
setHeight(isExpanded ? 200 : 0)
}, [isExpanded])
setHeight(isExpanded ? 200 : 0);
}, [isExpanded]);
// Correct: derive from state
const [isExpanded, setIsExpanded] = useState(false)
const height = isExpanded ? 200 : 0
const [isExpanded, setIsExpanded] = useState(false);
const height = isExpanded ? 200 : 0;
```
State is the minimal truth. Everything else is derived.
@@ -12,20 +12,20 @@ Use `expo-image` instead of React Native's `Image`. It provides memory-efficient
**Incorrect (React Native Image):**
```tsx
import { Image } from 'react-native'
import { Image } from 'react-native';
function Avatar({ url }: { url: string }) {
return <Image source={{ uri: url }} style={styles.avatar} />
return <Image source={{ uri: url }} style={styles.avatar} />;
}
```
**Correct (expo-image):**
```tsx
import { Image } from 'expo-image'
import { Image } from 'expo-image';
function Avatar({ url }: { url: string }) {
return <Image source={{ uri: url }} style={styles.avatar} />
return <Image source={{ uri: url }} style={styles.avatar} />;
}
```
@@ -44,12 +44,7 @@ function Avatar({ url }: { url: string }) {
**With priority and caching:**
```tsx
<Image
source={{ uri: url }}
priority="high"
cachePolicy="memory-disk"
style={styles.hero}
/>
<Image source={{ uri: url }} priority="high" cachePolicy="memory-disk" style={styles.hero} />
```
**Key props:**
@@ -1,8 +1,7 @@
---
title: Use Galeria for Image Galleries and Lightbox
impact: MEDIUM
impactDescription:
native shared element transitions, pinch-to-zoom, pan-to-close
impactDescription: native shared element transitions, pinch-to-zoom, pan-to-close
tags: images, gallery, lightbox, expo-image, ui
---
@@ -16,7 +15,7 @@ zoom, and pan-to-close. Works with any image component including `expo-image`.
```tsx
function ImageGallery({ urls }: { urls: string[] }) {
const [selected, setSelected] = useState<string | null>(null)
const [selected, setSelected] = useState<string | null>(null);
return (
<>
@@ -29,15 +28,15 @@ function ImageGallery({ urls }: { urls: string[] }) {
<Image source={{ uri: selected! }} style={styles.fullscreen} />
</Modal>
</>
)
);
}
```
**Correct (Galeria with expo-image):**
```tsx
import { Galeria } from '@nandorojo/galeria'
import { Image } from 'expo-image'
import { Galeria } from '@nandorojo/galeria';
import { Image } from 'expo-image';
function ImageGallery({ urls }: { urls: string[] }) {
return (
@@ -48,15 +47,15 @@ function ImageGallery({ urls }: { urls: string[] }) {
</Galeria.Image>
))}
</Galeria>
)
);
}
```
**Single image:**
```tsx
import { Galeria } from '@nandorojo/galeria'
import { Image } from 'expo-image'
import { Galeria } from '@nandorojo/galeria';
import { Image } from 'expo-image';
function Avatar({ url }: { url: string }) {
return (
@@ -65,7 +64,7 @@ function Avatar({ url }: { url: string }) {
<Image source={{ uri: url }} style={styles.avatar} />
</Galeria.Image>
</Galeria>
)
);
}
```
@@ -15,63 +15,63 @@ compare values and avoid unnecessary re-renders.
**Height only:**
```tsx
import { useLayoutEffect, useRef, useState } from 'react'
import { View, LayoutChangeEvent } from 'react-native'
import { useLayoutEffect, useRef, useState } from 'react';
import { View, LayoutChangeEvent } from 'react-native';
function MeasuredBox({ children }: { children: React.ReactNode }) {
const ref = useRef<View>(null)
const [height, setHeight] = useState<number | undefined>(undefined)
const ref = useRef<View>(null);
const [height, setHeight] = useState<number | undefined>(undefined);
useLayoutEffect(() => {
// Sync measurement on mount (RN 0.82+)
const rect = ref.current?.getBoundingClientRect()
if (rect) setHeight(rect.height)
const rect = ref.current?.getBoundingClientRect();
if (rect) setHeight(rect.height);
// Pre-0.82: ref.current?.measure((x, y, w, h) => setHeight(h))
}, [])
}, []);
const onLayout = (e: LayoutChangeEvent) => {
setHeight(e.nativeEvent.layout.height)
}
setHeight(e.nativeEvent.layout.height);
};
return (
<View ref={ref} onLayout={onLayout}>
{children}
</View>
)
);
}
```
**Both dimensions:**
```tsx
import { useLayoutEffect, useRef, useState } from 'react'
import { View, LayoutChangeEvent } from 'react-native'
import { useLayoutEffect, useRef, useState } from 'react';
import { View, LayoutChangeEvent } from 'react-native';
type Size = { width: number; height: number }
type Size = { width: number; height: number };
function MeasuredBox({ children }: { children: React.ReactNode }) {
const ref = useRef<View>(null)
const [size, setSize] = useState<Size | undefined>(undefined)
const ref = useRef<View>(null);
const [size, setSize] = useState<Size | undefined>(undefined);
useLayoutEffect(() => {
const rect = ref.current?.getBoundingClientRect()
if (rect) setSize({ width: rect.width, height: rect.height })
}, [])
const rect = ref.current?.getBoundingClientRect();
if (rect) setSize({ width: rect.width, height: rect.height });
}, []);
const onLayout = (e: LayoutChangeEvent) => {
const { width, height } = e.nativeEvent.layout
const { width, height } = e.nativeEvent.layout;
setSize((prev) => {
// for non-primitive states, compare values before firing a re-render
if (prev?.width === width && prev?.height === height) return prev
return { width, height }
})
}
if (prev?.width === width && prev?.height === height) return prev;
return { width, height };
});
};
return (
<View ref={ref} onLayout={onLayout}>
{children}
</View>
)
);
}
```
@@ -14,11 +14,11 @@ Use [zeego](https://zeego.dev) for cross-platform native menus.
**Incorrect (custom JS menu):**
```tsx
import { useState } from 'react'
import { View, Pressable, Text } from 'react-native'
import { useState } from 'react';
import { View, Pressable, Text } from 'react-native';
function MyMenu() {
const [open, setOpen] = useState(false)
const [open, setOpen] = useState(false);
return (
<View>
@@ -36,14 +36,14 @@ function MyMenu() {
</View>
)}
</View>
)
);
}
```
**Correct (native menu with zeego):**
```tsx
import * as DropdownMenu from 'zeego/dropdown-menu'
import * as DropdownMenu from 'zeego/dropdown-menu';
function MyMenu() {
return (
@@ -55,27 +55,23 @@ function MyMenu() {
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item key='edit' onSelect={() => console.log('edit')}>
<DropdownMenu.Item key="edit" onSelect={() => console.log('edit')}>
<DropdownMenu.ItemTitle>Edit</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
<DropdownMenu.Item
key='delete'
destructive
onSelect={() => console.log('delete')}
>
<DropdownMenu.Item key="delete" destructive onSelect={() => console.log('delete')}>
<DropdownMenu.ItemTitle>Delete</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
)
);
}
```
**Context menu (long-press):**
```tsx
import * as ContextMenu from 'zeego/context-menu'
import * as ContextMenu from 'zeego/context-menu';
function MyContextMenu() {
return (
@@ -87,26 +83,26 @@ function MyContextMenu() {
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item key='copy' onSelect={() => console.log('copy')}>
<ContextMenu.Item key="copy" onSelect={() => console.log('copy')}>
<ContextMenu.ItemTitle>Copy</ContextMenu.ItemTitle>
</ContextMenu.Item>
<ContextMenu.Item key='paste' onSelect={() => console.log('paste')}>
<ContextMenu.Item key="paste" onSelect={() => console.log('paste')}>
<ContextMenu.ItemTitle>Paste</ContextMenu.ItemTitle>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
)
);
}
```
**Checkbox items:**
```tsx
import * as DropdownMenu from 'zeego/dropdown-menu'
import * as DropdownMenu from 'zeego/dropdown-menu';
function SettingsMenu() {
const [notifications, setNotifications] = useState(true)
const [notifications, setNotifications] = useState(true);
return (
<DropdownMenu.Root>
@@ -118,7 +114,7 @@ function SettingsMenu() {
<DropdownMenu.Content>
<DropdownMenu.CheckboxItem
key='notifications'
key="notifications"
value={notifications}
onValueChange={() => setNotifications((prev) => !prev)}
>
@@ -127,14 +123,14 @@ function SettingsMenu() {
</DropdownMenu.CheckboxItem>
</DropdownMenu.Content>
</DropdownMenu.Root>
)
);
}
```
**Submenus:**
```tsx
import * as DropdownMenu from 'zeego/dropdown-menu'
import * as DropdownMenu from 'zeego/dropdown-menu';
function MenuWithSubmenu() {
return (
@@ -146,28 +142,28 @@ function MenuWithSubmenu() {
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item key='home' onSelect={() => console.log('home')}>
<DropdownMenu.Item key="home" onSelect={() => console.log('home')}>
<DropdownMenu.ItemTitle>Home</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger key='more'>
<DropdownMenu.SubTrigger key="more">
<DropdownMenu.ItemTitle>More Options</DropdownMenu.ItemTitle>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent>
<DropdownMenu.Item key='settings'>
<DropdownMenu.Item key="settings">
<DropdownMenu.ItemTitle>Settings</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
<DropdownMenu.Item key='help'>
<DropdownMenu.Item key="help">
<DropdownMenu.ItemTitle>Help</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
</DropdownMenu.Content>
</DropdownMenu.Root>
)
);
}
```
@@ -15,39 +15,39 @@ for low-level primitives.
**Incorrect (JS-based bottom sheet):**
```tsx
import BottomSheet from 'custom-js-bottom-sheet'
import BottomSheet from 'custom-js-bottom-sheet';
function MyScreen() {
const sheetRef = useRef<BottomSheet>(null)
const sheetRef = useRef<BottomSheet>(null);
return (
<View style={{ flex: 1 }}>
<Button onPress={() => sheetRef.current?.expand()} title='Open' />
<Button onPress={() => sheetRef.current?.expand()} title="Open" />
<BottomSheet ref={sheetRef} snapPoints={['50%', '90%']}>
<View>
<Text>Sheet content</Text>
</View>
</BottomSheet>
</View>
)
);
}
```
**Correct (native Modal with formSheet):**
```tsx
import { Modal, View, Text, Button } from 'react-native'
import { Modal, View, Text, Button } from 'react-native';
function MyScreen() {
const [visible, setVisible] = useState(false)
const [visible, setVisible] = useState(false);
return (
<View style={{ flex: 1 }}>
<Button onPress={() => setVisible(true)} title='Open' />
<Button onPress={() => setVisible(true)} title="Open" />
<Modal
visible={visible}
presentationStyle='formSheet'
animationType='slide'
presentationStyle="formSheet"
animationType="slide"
onRequestClose={() => setVisible(false)}
>
<View>
@@ -55,7 +55,7 @@ function MyScreen() {
</View>
</Modal>
</View>
)
);
}
```
@@ -64,7 +64,7 @@ function MyScreen() {
```tsx
// In your navigator
<Stack.Screen
name='Details'
name="Details"
component={DetailsScreen}
options={{
presentation: 'formSheet',
@@ -13,42 +13,42 @@ Never use `TouchableOpacity` or `TouchableHighlight`. Use `Pressable` from
**Incorrect (legacy Touchable components):**
```tsx
import { TouchableOpacity } from 'react-native'
import { TouchableOpacity } from 'react-native';
function MyButton({ onPress }: { onPress: () => void }) {
return (
<TouchableOpacity onPress={onPress} activeOpacity={0.7}>
<Text>Press me</Text>
</TouchableOpacity>
)
);
}
```
**Correct (Pressable):**
```tsx
import { Pressable } from 'react-native'
import { Pressable } from 'react-native';
function MyButton({ onPress }: { onPress: () => void }) {
return (
<Pressable onPress={onPress}>
<Text>Press me</Text>
</Pressable>
)
);
}
```
**Correct (Pressable from gesture handler for lists):**
```tsx
import { Pressable } from 'react-native-gesture-handler'
import { Pressable } from 'react-native-gesture-handler';
function ListItem({ onPress }: { onPress: () => void }) {
return (
<Pressable onPress={onPress}>
<Text>Item</Text>
</Pressable>
)
);
}
```
@@ -12,7 +12,7 @@ Use `contentInsetAdjustmentBehavior="automatic"` on the root ScrollView instead
**Incorrect (SafeAreaView wrapper):**
```tsx
import { SafeAreaView, ScrollView, View, Text } from 'react-native'
import { SafeAreaView, ScrollView, View, Text } from 'react-native';
function MyScreen() {
return (
@@ -23,18 +23,18 @@ function MyScreen() {
</View>
</ScrollView>
</SafeAreaView>
)
);
}
```
**Incorrect (manual safe area padding):**
```tsx
import { ScrollView, View, Text } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { ScrollView, View, Text } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function MyScreen() {
const insets = useSafeAreaInsets()
const insets = useSafeAreaInsets();
return (
<ScrollView contentContainerStyle={{ paddingTop: insets.top }}>
@@ -42,23 +42,23 @@ function MyScreen() {
<Text>Content</Text>
</View>
</ScrollView>
)
);
}
```
**Correct (native content inset adjustment):**
```tsx
import { ScrollView, View, Text } from 'react-native'
import { ScrollView, View, Text } from 'react-native';
function MyScreen() {
return (
<ScrollView contentInsetAdjustmentBehavior='automatic'>
<ScrollView contentInsetAdjustmentBehavior="automatic">
<View>
<Text>Content</Text>
</View>
</ScrollView>
)
);
}
```
@@ -17,10 +17,8 @@ scroll area without re-rendering content.
```tsx
function Feed({ bottomOffset }: { bottomOffset: number }) {
return (
<ScrollView contentContainerStyle={{ paddingBottom: bottomOffset }}>
{children}
</ScrollView>
)
<ScrollView contentContainerStyle={{ paddingBottom: bottomOffset }}>{children}</ScrollView>
);
}
// Changing bottomOffset triggers full layout recalculation
```
@@ -36,7 +34,7 @@ function Feed({ bottomOffset }: { bottomOffset: number }) {
>
{children}
</ScrollView>
)
);
}
// Changing bottomOffset only adjusts scroll bounds
```