

Also from Kynth Studios


Also from Kynth Studios


Also from Kynth Studios
1<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:_sections:start -->2# Sections34This file defines all sections, their ordering, impact levels, and descriptions.5The section ID (in parentheses) is the filename prefix used to group rules.67---89## 1. Core Rendering (rendering)1011**Impact:** CRITICAL12**Description:** Fundamental React Native rendering rules. Violations cause13runtime crashes or broken UI.1415## 2. List Performance (list-performance)1617**Impact:** HIGH18**Description:** Optimizing virtualized lists (FlatList, LegendList, FlashList)19for smooth scrolling and fast updates.2021## 3. Animation (animation)2223**Impact:** HIGH24**Description:** GPU-accelerated animations, Reanimated patterns, and avoiding25render thrashing during gestures.2627## 4. Scroll Performance (scroll)2829**Impact:** HIGH30**Description:** Tracking scroll position without causing render thrashing.3132## 5. Navigation (navigation)3334**Impact:** HIGH35**Description:** Using native navigators for stack and tab navigation instead of36JS-based alternatives.3738## 6. React State (react-state)3940**Impact:** MEDIUM41**Description:** Patterns for managing React state to avoid stale closures and42unnecessary re-renders.4344## 7. State Architecture (state)4546**Impact:** MEDIUM47**Description:** Ground truth principles for state variables and derived values.4849## 8. React Compiler (react-compiler)5051**Impact:** MEDIUM52**Description:** Compatibility patterns for React Compiler with React Native and53Reanimated.5455## 9. User Interface (ui)5657**Impact:** MEDIUM58**Description:** Native UI patterns for images, menus, modals, styling, and59platform-consistent interfaces.6061## 10. Design System (design-system)6263**Impact:** MEDIUM64**Description:** Architecture patterns for building maintainable component65libraries.6667## 11. Monorepo (monorepo)6869**Impact:** LOW70**Description:** Dependency management and native module configuration in71monorepos.7273## 12. Third-Party Dependencies (imports)7475**Impact:** LOW76**Description:** Wrapping and re-exporting third-party dependencies for77maintainability.7879## 13. JavaScript (js)8081**Impact:** LOW82**Description:** Micro-optimizations like hoisting expensive object creation.8384## 14. Fonts (fonts)8586**Impact:** LOW87**Description:** Native font loading for improved performance.88<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:_sections:end -->8990<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:_template:start -->91## Rule Title Here9293**Impact: MEDIUM (optional impact description)**9495Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.9697**Incorrect (description of what's wrong):**9899```typescript100// Bad code example here101const bad = example()102```103104**Correct (description of what's right):**105106```typescript107// Good code example here108const good = example()109```110111Reference: [Link to documentation or resource](https://example.com)112<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:_template:end -->113114<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:animation-derived-value:start -->115## Prefer useDerivedValue Over useAnimatedReaction116117When deriving a shared value from another, use `useDerivedValue` instead of118`useAnimatedReaction`. Derived values are declarative, automatically track119dependencies, and return a value you can use directly. Animated reactions are120for side effects, not derivations.121122**Incorrect (useAnimatedReaction for derivation):**123124```tsx125import { useSharedValue, useAnimatedReaction } from 'react-native-reanimated'126127function MyComponent() {128 const progress = useSharedValue(0)129 const opacity = useSharedValue(1)130131 useAnimatedReaction(132 () => progress.value,133 (current) => {134 opacity.value = 1 - current135 }136 )137138 // ...139}140```141142**Correct (useDerivedValue):**143144```tsx145import { useSharedValue, useDerivedValue } from 'react-native-reanimated'146147function MyComponent() {148 const progress = useSharedValue(0)149150 const opacity = useDerivedValue(() => 1 - progress.get())151152 // ...153}154```155156Use `useAnimatedReaction` only for side effects that don't produce a value157(e.g., triggering haptics, logging, calling `runOnJS`).158159Reference:160[Reanimated useDerivedValue](https://docs.swmansion.com/react-native-reanimated/docs/core/useDerivedValue)161<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:animation-derived-value:end -->162163<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:animation-gesture-detector-press:start -->164## Use GestureDetector for Animated Press States165166For animated press states (scale, opacity on press), use `GestureDetector` with167`Gesture.Tap()` and shared values instead of Pressable's168`onPressIn`/`onPressOut`. Gesture callbacks run on the UI thread as worklets—no169JS thread round-trip for press animations.170171**Incorrect (Pressable with JS thread callbacks):**172173```tsx174import { Pressable } from 'react-native'175import Animated, {176 useSharedValue,177 useAnimatedStyle,178 withTiming,179} from 'react-native-reanimated'180181function AnimatedButton({ onPress }: { onPress: () => void }) {182 const scale = useSharedValue(1)183184 const animatedStyle = useAnimatedStyle(() => ({185 transform: [{ scale: scale.value }],186 }))187188 return (189 <Pressable190 onPress={onPress}191 onPressIn={() => (scale.value = withTiming(0.95))}192 onPressOut={() => (scale.value = withTiming(1))}193 >194 <Animated.View style={animatedStyle}>195 <Text>Press me</Text>196 </Animated.View>197 </Pressable>198 )199}200```201202**Correct (GestureDetector with UI thread worklets):**203204```tsx205import { Gesture, GestureDetector } from 'react-native-gesture-handler'206import Animated, {207 useSharedValue,208 useAnimatedStyle,209 withTiming,210 interpolate,211 runOnJS,212} from 'react-native-reanimated'213214function AnimatedButton({ onPress }: { onPress: () => void }) {215 // Store the press STATE (0 = not pressed, 1 = pressed)216 const pressed = useSharedValue(0)217218 const tap = Gesture.Tap()219 .onBegin(() => {220 pressed.set(withTiming(1))221 })222 .onFinalize(() => {223 pressed.set(withTiming(0))224 })225 .onEnd(() => {226 runOnJS(onPress)()227 })228229 // Derive visual values from the state230 const animatedStyle = useAnimatedStyle(() => ({231 transform: [232 { scale: interpolate(withTiming(pressed.get()), [0, 1], [1, 0.95]) },233 ],234 }))235236 return (237 <GestureDetector gesture={tap}>238 <Animated.View style={animatedStyle}>239 <Text>Press me</Text>240 </Animated.View>241 </GestureDetector>242 )243}244```245246Store the press **state** (0 or 1), then derive the scale via `interpolate`.247This keeps the shared value as ground truth. Use `runOnJS` to call JS functions248from worklets. Use `.set()` and `.get()` for React Compiler compatibility.249250Reference:251[Gesture Handler Tap Gesture](https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture)252<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:animation-gesture-detector-press:end -->253254<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:animation-gpu-properties:start -->255## Animate Transform and Opacity Instead of Layout Properties256257Avoid animating `width`, `height`, `top`, `left`, `margin`, or `padding`. These trigger layout recalculation on every frame. Instead, use `transform` (scale, translate) and `opacity` which run on the GPU without triggering layout.258259**Incorrect (animates height, triggers layout every frame):**260261```tsx262import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'263264function CollapsiblePanel({ expanded }: { expanded: boolean }) {265 const animatedStyle = useAnimatedStyle(() => ({266 height: withTiming(expanded ? 200 : 0), // triggers layout on every frame267 overflow: 'hidden',268 }))269270 return <Animated.View style={animatedStyle}>{children}</Animated.View>271}272```273274**Correct (animates scaleY, GPU-accelerated):**275276```tsx277import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'278279function CollapsiblePanel({ expanded }: { expanded: boolean }) {280 const animatedStyle = useAnimatedStyle(() => ({281 transform: [282 { scaleY: withTiming(expanded ? 1 : 0) },283 ],284 opacity: withTiming(expanded ? 1 : 0),285 }))286287 return (288 <Animated.View style={[{ height: 200, transformOrigin: 'top' }, animatedStyle]}>289 {children}290 </Animated.View>291 )292}293```294295**Correct (animates translateY for slide animations):**296297```tsx298import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'299300function SlideIn({ visible }: { visible: boolean }) {301 const animatedStyle = useAnimatedStyle(() => ({302 transform: [303 { translateY: withTiming(visible ? 0 : 100) },304 ],305 opacity: withTiming(visible ? 1 : 0),306 }))307308 return <Animated.View style={animatedStyle}>{children}</Animated.View>309}310```311312GPU-accelerated properties: `transform` (translate, scale, rotate), `opacity`. Everything else triggers layout.313<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:animation-gpu-properties:end -->314315<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:design-system-compound-components:start -->316## Use Compound Components Over Polymorphic Children317318Don't create components that can accept a string if they aren't a text node. If319a component can receive a string child, it must be a dedicated `*Text`320component. For components like buttons, which can have both a View (or321Pressable) together with text, use compound components, such a `Button`,322`ButtonText`, and `ButtonIcon`.323324**Incorrect (polymorphic children):**325326```tsx327import { Pressable, Text } from 'react-native'328329type ButtonProps = {330 children: string | React.ReactNode331 icon?: React.ReactNode332}333334function Button({ children, icon }: ButtonProps) {335 return (336 <Pressable>337 {icon}338 {typeof children === 'string' ? <Text>{children}</Text> : children}339 </Pressable>340 )341}342343// Usage is ambiguous344<Button icon={<Icon />}>Save</Button>345<Button><CustomText>Save</CustomText></Button>346```347348**Correct (compound components):**349350```tsx351import { Pressable, Text } from 'react-native'352353function Button({ children }: { children: React.ReactNode }) {354 return <Pressable>{children}</Pressable>355}356357function ButtonText({ children }: { children: React.ReactNode }) {358 return <Text>{children}</Text>359}360361function ButtonIcon({ children }: { children: React.ReactNode }) {362 return <>{children}</>363}364365// Usage is explicit and composable366<Button>367 <ButtonIcon><SaveIcon /></ButtonIcon>368 <ButtonText>Save</ButtonText>369</Button>370371<Button>372 <ButtonText>Cancel</ButtonText>373</Button>374```375<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:design-system-compound-components:end -->376377<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:fonts-config-plugin:start -->378## Use Expo Config Plugin for Font Loading379380Use the `expo-font` config plugin to embed fonts at build time instead of381`useFonts` or `Font.loadAsync`. Embedded fonts are more efficient.382383**Incorrect (async font loading):**384385```tsx386import { useFonts } from 'expo-font'387import { Text, View } from 'react-native'388389function App() {390 const [fontsLoaded] = useFonts({391 'Geist-Bold': require('./assets/fonts/Geist-Bold.otf'),392 })393394 if (!fontsLoaded) {395 return null396 }397398 return (399 <View>400 <Text style={{ fontFamily: 'Geist-Bold' }}>Hello</Text>401 </View>402 )403}404```405406**Correct (config plugin, fonts embedded at build):**407408```json409// app.json410{411 "expo": {412 "plugins": [413 [414 "expo-font",415 {416 "fonts": ["./assets/fonts/Geist-Bold.otf"]417 }418 ]419 ]420 }421}422```423424```tsx425import { Text, View } from 'react-native'426427function App() {428 // No loading state needed—font is already available429 return (430 <View>431 <Text style={{ fontFamily: 'Geist-Bold' }}>Hello</Text>432 </View>433 )434}435```436437After adding fonts to the config plugin, run `npx expo prebuild` and rebuild the438native app.439440Reference:441[Expo Font Documentation](https://docs.expo.dev/versions/latest/sdk/font/)442<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:fonts-config-plugin:end -->443444<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:imports-design-system-folder:start -->445## Import from Design System Folder446447Re-export dependencies from a design system folder. App code imports from there,448not directly from packages. This enables global changes and easy refactoring.449450**Incorrect (imports directly from package):**451452```tsx453import { View, Text } from 'react-native'454import { Button } from '@ui/button'455456function Profile() {457 return (458 <View>459 <Text>Hello</Text>460 <Button>Save</Button>461 </View>462 )463}464```465466**Correct (imports from design system):**467468```tsx469// components/view.tsx470import { View as RNView } from 'react-native'471472// ideal: pick the props you will actually use to control implementation473export function View(474 props: Pick<React.ComponentProps<typeof RNView>, 'style' | 'children'>475) {476 return <RNView {...props} />477}478```479480```tsx481// components/text.tsx482export { Text } from 'react-native'483```484485```tsx486// components/button.tsx487export { Button } from '@ui/button'488```489490```tsx491import { View } from '@/components/view'492import { Text } from '@/components/text'493import { Button } from '@/components/button'494495function Profile() {496 return (497 <View>498 <Text>Hello</Text>499 <Button>Save</Button>500 </View>501 )502}503```504505Start by simply re-exporting. Customize later without changing app code.506<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:imports-design-system-folder:end -->507508<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:js-hoist-intl:start -->509## Hoist Intl Formatter Creation510511Don't create `Intl.DateTimeFormat`, `Intl.NumberFormat`, or512`Intl.RelativeTimeFormat` inside render or loops. These are expensive to513instantiate. Hoist to module scope when the locale/options are static.514515**Incorrect (new formatter every render):**516517```tsx518function Price({ amount }: { amount: number }) {519 const formatter = new Intl.NumberFormat('en-US', {520 style: 'currency',521 currency: 'USD',522 })523 return <Text>{formatter.format(amount)}</Text>524}525```526527**Correct (hoisted to module scope):**528529```tsx530const currencyFormatter = new Intl.NumberFormat('en-US', {531 style: 'currency',532 currency: 'USD',533})534535function Price({ amount }: { amount: number }) {536 return <Text>{currencyFormatter.format(amount)}</Text>537}538```539540**For dynamic locales, memoize:**541542```tsx543const dateFormatter = useMemo(544 () => new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }),545 [locale]546)547```548549**Common formatters to hoist:**550551```tsx552// Module-level formatters553const dateFormatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' })554const timeFormatter = new Intl.DateTimeFormat('en-US', { timeStyle: 'short' })555const percentFormatter = new Intl.NumberFormat('en-US', { style: 'percent' })556const relativeFormatter = new Intl.RelativeTimeFormat('en-US', {557 numeric: 'auto',558})559```560561Creating `Intl` objects is significantly more expensive than `RegExp` or plain562objects—each instantiation parses locale data and builds internal lookup tables.563<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:js-hoist-intl:end -->564565<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:list-performance-callbacks:start -->566## List performance callbacks567568**Impact: HIGH (Fewer re-renders and faster lists)**569570When passing callback functions to list items, create a single instance of the571callback at the root of the list. Items should then call it with a unique572identifier.573574**Incorrect (creates a new callback on each render):**575576```typescript577return (578 <LegendList579 renderItem={({ item }) => {580 // bad: creates a new callback on each render581 const onPress = () => handlePress(item.id)582 return <Item key={item.id} item={item} onPress={onPress} />583 }}584 />585)586```587588**Correct (a single function instance passed to each item):**589590```typescript591const onPress = useCallback(() => handlePress(item.id), [handlePress, item.id])592593return (594 <LegendList595 renderItem={({ item }) => (596 <Item key={item.id} item={item} onPress={onPress} />597 )}598 />599)600```601602Reference: [Link to documentation or resource](https://example.com)603<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:list-performance-callbacks:end -->604605<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:list-performance-function-references:start -->606## Optimize List Performance with Stable Object References607608Don't map or filter data before passing to virtualized lists. Virtualization609relies on object reference stability to know what changed—new references cause610full re-renders of all visible items. Attempt to prevent frequent renders at the611list-parent level.612613Where needed, use context selectors within list items.614615**Incorrect (creates new object references on every keystroke):**616617```tsx618function DomainSearch() {619 const { keyword, setKeyword } = useKeywordZustandState()620 const { data: tlds } = useTlds()621622 // Bad: creates new objects on every render, reparenting the entire list on every keystroke623 const domains = tlds.map((tld) => ({624 domain: `${keyword}.${tld.name}`,625 tld: tld.name,626 price: tld.price,627 }))628629 return (630 <>631 <TextInput value={keyword} onChangeText={setKeyword} />632 <LegendList633 data={domains}634 renderItem={({ item }) => <DomainItem item={item} keyword={keyword} />}635 />636 </>637 )638}639```640641**Correct (stable references, transform inside items):**642643```tsx644const renderItem = ({ item }) => <DomainItem tld={item} />645646function DomainSearch() {647 const { data: tlds } = useTlds()648649 return (650 <LegendList651 // good: as long as the data is stable, LegendList will not re-render the entire list652 data={tlds}653 renderItem={renderItem}654 />655 )656}657658function DomainItem({ tld }: { tld: Tld }) {659 // good: transform within items, and don't pass the dynamic data as a prop660 // good: use a selector function from zustand to receive a stable string back661 const domain = useKeywordZustandState((s) => s.keyword + '.' + tld.name)662 return <Text>{domain}</Text>663}664```665666**Updating parent array reference:**667668Creating a new array instance can be okay, as long as its inner object669references are stable. For instance, if you sort a list of objects:670671```tsx672// good: creates a new array instance without mutating the inner objects673// good: parent array reference is unaffected by typing and updating "keyword"674const sortedTlds = tlds.toSorted((a, b) => a.name.localeCompare(b.name))675676return <LegendList data={sortedTlds} renderItem={renderItem} />677```678679Even though this creates a new array instance `sortedTlds`, the inner object680references are stable.681682**With zustand for dynamic data (avoids parent re-renders):**683684```tsx685const useSearchStore = create<{ keyword: string }>(() => ({ keyword: '' }))686687function DomainSearch() {688 const { data: tlds } = useTlds()689690 return (691 <>692 <SearchInput />693 <LegendList694 data={tlds}695 // if you aren't using React Compiler, wrap renderItem with useCallback696 renderItem={({ item }) => <DomainItem tld={item} />}697 />698 </>699 )700}701702function DomainItem({ tld }: { tld: Tld }) {703 // Select only what you need—component only re-renders when keyword changes704 const keyword = useSearchStore((s) => s.keyword)705 const domain = `${keyword}.${tld.name}`706 return <Text>{domain}</Text>707}708```709710Virtualization can now skip items that haven't changed when typing. Only visible711items (~20) re-render on keystroke, rather than the parent.712713**Deriving state within list items based on parent data (avoids parent714re-renders):**715716For components where the data is conditional based on the parent state, this717pattern is even more important. For example, if you are checking if an item is718favorited, toggling favorites only re-renders one component if the item itself719is in charge of accessing the state rather than the parent:720721```tsx722function DomainItemFavoriteButton({ tld }: { tld: Tld }) {723 const isFavorited = useFavoritesStore((s) => s.favorites.has(tld.id))724 return <TldFavoriteButton isFavorited={isFavorited} />725}726```727728Note: if you're using the React Compiler, you can read React Context values729directly within list items. Although this is slightly slower than using a730Zustand selector in most cases, the effect may be negligible.731<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:list-performance-function-references:end -->732733<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:list-performance-images:start -->734## Use Compressed Images in Lists735736Always load compressed, appropriately-sized images in lists. Full-resolution737images consume excessive memory and cause scroll jank. Request thumbnails from738your server or use an image CDN with resize parameters.739740**Incorrect (full-resolution images):**741742```tsx743function ProductItem({ product }: { product: Product }) {744 return (745 <View>746 {/* 4000x3000 image loaded for a 100x100 thumbnail */}747 <Image748 source={{ uri: product.imageUrl }}749 style={{ width: 100, height: 100 }}750 />751 <Text>{product.name}</Text>752 </View>753 )754}755```756757**Correct (request appropriately-sized image):**758759```tsx760function ProductItem({ product }: { product: Product }) {761 // Request a 200x200 image (2x for retina)762 const thumbnailUrl = `${product.imageUrl}?w=200&h=200&fit=cover`763764 return (765 <View>766 <Image767 source={{ uri: thumbnailUrl }}768 style={{ width: 100, height: 100 }}769 contentFit='cover'770 />771 <Text>{product.name}</Text>772 </View>773 )774}775```776777Use an optimized image component with built-in caching and placeholder support,778such as `expo-image` or `SolitoImage` (which uses `expo-image` under the hood).779Request images at 2x the display size for retina screens.780<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:list-performance-images:end -->781782<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:list-performance-inline-objects:start -->783## Avoid Inline Objects in renderItem784785Don't create new objects inside `renderItem` to pass as props. Inline objects786create new references on every render, breaking memoization. Pass primitive787values directly from `item` instead.788789**Incorrect (inline object breaks memoization):**790791```tsx792function UserList({ users }: { users: User[] }) {793 return (794 <LegendList795 data={users}796 renderItem={({ item }) => (797 <UserRow798 // Bad: new object on every render799 user={{ id: item.id, name: item.name, avatar: item.avatar }}800 />801 )}802 />803 )804}805```806807**Incorrect (inline style object):**808809```tsx810renderItem={({ item }) => (811 <UserRow812 name={item.name}813 // Bad: new style object on every render814 style={{ backgroundColor: item.isActive ? 'green' : 'gray' }}815 />816)}817```818819**Correct (pass item directly or primitives):**820821```tsx822function UserList({ users }: { users: User[] }) {823 return (824 <LegendList825 data={users}826 renderItem={({ item }) => (827 // Good: pass the item directly828 <UserRow user={item} />829 )}830 />831 )832}833```834835**Correct (pass primitives, derive inside child):**836837```tsx838renderItem={({ item }) => (839 <UserRow840 id={item.id}841 name={item.name}842 isActive={item.isActive}843 />844)}845846const UserRow = memo(function UserRow({ id, name, isActive }: Props) {847 // Good: derive style inside memoized component848 const backgroundColor = isActive ? 'green' : 'gray'849 return <View style={[styles.row, { backgroundColor }]}>{/* ... */}</View>850})851```852853**Correct (hoist static styles in module scope):**854855```tsx856const activeStyle = { backgroundColor: 'green' }857const inactiveStyle = { backgroundColor: 'gray' }858859renderItem={({ item }) => (860 <UserRow861 name={item.name}862 // Good: stable references863 style={item.isActive ? activeStyle : inactiveStyle}864 />865)}866```867868Passing primitives or stable references allows `memo()` to skip re-renders when869the actual values haven't changed.870871**Note:** If you have the React Compiler enabled, it handles memoization872automatically and these manual optimizations become less critical.873<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:list-performance-inline-objects:end -->874875<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:list-performance-item-expensive:start -->876## Keep List Items Lightweight877878List items should be as inexpensive as possible to render. Minimize hooks, avoid879queries, and limit React Context access. Virtualized lists render many items880during scroll—expensive items cause jank.881882**Incorrect (heavy list item):**883884```tsx885function ProductRow({ id }: { id: string }) {886 // Bad: query inside list item887 const { data: product } = useQuery(['product', id], () => fetchProduct(id))888 // Bad: multiple context accesses889 const theme = useContext(ThemeContext)890 const user = useContext(UserContext)891 const cart = useContext(CartContext)892 // Bad: expensive computation893 const recommendations = useMemo(894 () => computeRecommendations(product),895 [product]896 )897898 return <View>{/* ... */}</View>899}900```901902**Correct (lightweight list item):**903904```tsx905function ProductRow({ name, price, imageUrl }: Props) {906 // Good: receives only primitives, minimal hooks907 return (908 <View>909 <Image source={{ uri: imageUrl }} />910 <Text>{name}</Text>911 <Text>{price}</Text>912 </View>913 )914}915```916917**Move data fetching to parent:**918919```tsx920// Parent fetches all data once921function ProductList() {922 const { data: products } = useQuery(['products'], fetchProducts)923924 return (925 <LegendList926 data={products}927 renderItem={({ item }) => (928 <ProductRow name={item.name} price={item.price} imageUrl={item.image} />929 )}930 />931 )932}933```934935**For shared values, use Zustand selectors instead of Context:**936937```tsx938// Incorrect: Context causes re-render when any cart value changes939function ProductRow({ id, name }: Props) {940 const { items } = useContext(CartContext)941 const inCart = items.includes(id)942 // ...943}944945// Correct: Zustand selector only re-renders when this specific value changes946function ProductRow({ id, name }: Props) {947 // use Set.has (created once at the root) instead of Array.includes()948 const inCart = useCartStore((s) => s.items.has(id))949 // ...950}951```952953**Guidelines for list items:**954955- No queries or data fetching956- No expensive computations (move to parent or memoize at parent level)957- Prefer Zustand selectors over React Context958- Minimize useState/useEffect hooks959- Pass pre-computed values as props960961The goal: list items should be simple rendering functions that take props and962return JSX.963<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:list-performance-item-expensive:end -->964965<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:list-performance-item-memo:start -->966## Pass Primitives to List Items for Memoization967968When possible, pass only primitive values (strings, numbers, booleans) as props969to list item components. Primitives enable shallow comparison in `memo()` to970work correctly, skipping re-renders when values haven't changed.971972**Incorrect (object prop requires deep comparison):**973974```tsx975type User = { id: string; name: string; email: string; avatar: string }976977const UserRow = memo(function UserRow({ user }: { user: User }) {978 // memo() compares user by reference, not value979 // If parent creates new user object, this re-renders even if data is same980 return <Text>{user.name}</Text>981})982983renderItem={({ item }) => <UserRow user={item} />}984```985986This can still be optimized, but it is harder to memoize properly.987988**Correct (primitive props enable shallow comparison):**989990```tsx991const UserRow = memo(function UserRow({992 id,993 name,994 email,995}: {996 id: string997 name: string998 email: string999}) {1000 // memo() compares each primitive directly1001 // Re-renders only if id, name, or email actually changed1002 return <Text>{name}</Text>1003})10041005renderItem={({ item }) => (1006 <UserRow id={item.id} name={item.name} email={item.email} />1007)}1008```10091010**Pass only what you need:**10111012```tsx1013// Incorrect: passing entire item when you only need name1014<UserRow user={item} />10151016// Correct: pass only the fields the component uses1017<UserRow name={item.name} avatarUrl={item.avatar} />1018```10191020**For callbacks, hoist or use item ID:**10211022```tsx1023// Incorrect: inline function creates new reference1024<UserRow name={item.name} onPress={() => handlePress(item.id)} />10251026// Correct: pass ID, handle in child1027<UserRow id={item.id} name={item.name} />10281029const UserRow = memo(function UserRow({ id, name }: Props) {1030 const handlePress = useCallback(() => {1031 // use id here1032 }, [id])1033 return <Pressable onPress={handlePress}><Text>{name}</Text></Pressable>1034})1035```10361037Primitive props make memoization predictable and effective.10381039**Note:** If you have the React Compiler enabled, you do not need to use1040`memo()` or `useCallback()`, but the object references still apply.1041<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:list-performance-item-memo:end -->10421043<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:list-performance-item-types:start -->1044## Use Item Types for Heterogeneous Lists10451046When a list has different item layouts (messages, images, headers, etc.), use a1047`type` field on each item and provide `getItemType` to the list. This puts items1048into separate recycling pools so a message component never gets recycled into an1049image component.10501051**Incorrect (single component with conditionals):**10521053```tsx1054type Item = { id: string; text?: string; imageUrl?: string; isHeader?: boolean }10551056function ListItem({ item }: { item: Item }) {1057 if (item.isHeader) {1058 return <HeaderItem title={item.text} />1059 }1060 if (item.imageUrl) {1061 return <ImageItem url={item.imageUrl} />1062 }1063 return <MessageItem text={item.text} />1064}10651066function Feed({ items }: { items: Item[] }) {1067 return (1068 <LegendList1069 data={items}1070 renderItem={({ item }) => <ListItem item={item} />}1071 recycleItems1072 />1073 )1074}1075```10761077**Correct (typed items with separate components):**10781079```tsx1080type HeaderItem = { id: string; type: 'header'; title: string }1081type MessageItem = { id: string; type: 'message'; text: string }1082type ImageItem = { id: string; type: 'image'; url: string }1083type FeedItem = HeaderItem | MessageItem | ImageItem10841085function Feed({ items }: { items: FeedItem[] }) {1086 return (1087 <LegendList1088 data={items}1089 keyExtractor={(item) => item.id}1090 getItemType={(item) => item.type}1091 renderItem={({ item }) => {1092 switch (item.type) {1093 case 'header':1094 return <SectionHeader title={item.title} />1095 case 'message':1096 return <MessageRow text={item.text} />1097 case 'image':1098 return <ImageRow url={item.url} />1099 }1100 }}1101 recycleItems1102 />1103 )1104}1105```11061107**Why this matters:**11081109- **Recycling efficiency**: Items with the same type share a recycling pool1110- **No layout thrashing**: A header never recycles into an image cell1111- **Type safety**: TypeScript can narrow the item type in each branch1112- **Better size estimation**: Use `getEstimatedItemSize` with `itemType` for1113 accurate estimates per type11141115```tsx1116<LegendList1117 data={items}1118 keyExtractor={(item) => item.id}1119 getItemType={(item) => item.type}1120 getEstimatedItemSize={(index, item, itemType) => {1121 switch (itemType) {1122 case 'header':1123 return 481124 case 'message':1125 return 721126 case 'image':1127 return 3001128 default:1129 return 721130 }1131 }}1132 renderItem={({ item }) => {1133 /* ... */1134 }}1135 recycleItems1136/>1137```11381139Reference:1140[LegendList getItemType](https://legendapp.com/open-source/list/api/props/#getitemtype-v2)1141<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:list-performance-item-types:end -->11421143<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:list-performance-virtualize:start -->1144## Use a List Virtualizer for Any List11451146Use a list virtualizer like LegendList or FlashList instead of ScrollView with1147mapped children—even for short lists. Virtualizers only render visible items,1148reducing memory usage and mount time. ScrollView renders all children upfront,1149which gets expensive quickly.11501151**Incorrect (ScrollView renders all items at once):**11521153```tsx1154function Feed({ items }: { items: Item[] }) {1155 return (1156 <ScrollView>1157 {items.map((item) => (1158 <ItemCard key={item.id} item={item} />1159 ))}1160 </ScrollView>1161 )1162}1163// 50 items = 50 components mounted, even if only 10 visible1164```11651166**Correct (virtualizer renders only visible items):**11671168```tsx1169import { LegendList } from '@legendapp/list'11701171function Feed({ items }: { items: Item[] }) {1172 return (1173 <LegendList1174 data={items}1175 // if you aren't using React Compiler, wrap these with useCallback1176 renderItem={({ item }) => <ItemCard item={item} />}1177 keyExtractor={(item) => item.id}1178 estimatedItemSize={80}1179 />1180 )1181}1182// Only ~10-15 visible items mounted at a time1183```11841185**Alternative (FlashList):**11861187```tsx1188import { FlashList } from '@shopify/flash-list'11891190function Feed({ items }: { items: Item[] }) {1191 return (1192 <FlashList1193 data={items}1194 // if you aren't using React Compiler, wrap these with useCallback1195 renderItem={({ item }) => <ItemCard item={item} />}1196 keyExtractor={(item) => item.id}1197 />1198 )1199}1200```12011202Benefits apply to any screen with scrollable content—profiles, settings, feeds,1203search results. Default to virtualization.1204<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:list-performance-virtualize:end -->12051206<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:monorepo-native-deps-in-app:start -->1207## Install Native Dependencies in App Directory12081209In a monorepo, packages with native code must be installed in the native app's1210directory directly. Autolinking only scans the app's `node_modules`—it won't1211find native dependencies installed in other packages.12121213**Incorrect (native dep in shared package only):**12141215```1216packages/1217 ui/1218 package.json # has react-native-reanimated1219 app/1220 package.json # missing react-native-reanimated1221```12221223Autolinking fails—native code not linked.12241225**Correct (native dep in app directory):**12261227```1228packages/1229 ui/1230 package.json # has react-native-reanimated1231 app/1232 package.json # also has react-native-reanimated1233```12341235```json1236// packages/app/package.json1237{1238 "dependencies": {1239 "react-native-reanimated": "3.16.1"1240 }1241}1242```12431244Even if the shared package uses the native dependency, the app must also list it1245for autolinking to detect and link the native code.1246<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:monorepo-native-deps-in-app:end -->12471248<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:monorepo-single-dependency-versions:start -->1249## Use Single Dependency Versions Across Monorepo12501251Use a single version of each dependency across all packages in your monorepo.1252Prefer exact versions over ranges. Multiple versions cause duplicate code in1253bundles, runtime conflicts, and inconsistent behavior across packages.12541255Use a tool like syncpack to enforce this. As a last resort, use yarn resolutions1256or npm overrides.12571258**Incorrect (version ranges, multiple versions):**12591260```json1261// packages/app/package.json1262{1263 "dependencies": {1264 "react-native-reanimated": "^3.0.0"1265 }1266}12671268// packages/ui/package.json1269{1270 "dependencies": {1271 "react-native-reanimated": "^3.5.0"1272 }1273}1274```12751276**Correct (exact versions, single source of truth):**12771278```json1279// package.json (root)1280{1281 "pnpm": {1282 "overrides": {1283 "react-native-reanimated": "3.16.1"1284 }1285 }1286}12871288// packages/app/package.json1289{1290 "dependencies": {1291 "react-native-reanimated": "3.16.1"1292 }1293}12941295// packages/ui/package.json1296{1297 "dependencies": {1298 "react-native-reanimated": "3.16.1"1299 }1300}1301```13021303Use your package manager's override/resolution feature to enforce versions at1304the root. When adding dependencies, specify exact versions without `^` or `~`.1305<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:monorepo-single-dependency-versions:end -->13061307<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:navigation-native-navigators:start -->1308## Use Native Navigators for Navigation13091310Always use native navigators instead of JS-based ones. Native navigators use1311platform APIs (UINavigationController on iOS, Fragment on Android) for better1312performance and native behavior.13131314**For stacks:** Use `@react-navigation/native-stack` or expo-router's default1315stack (which uses native-stack). Avoid `@react-navigation/stack`.13161317**For tabs:** Use `react-native-bottom-tabs` (native) or expo-router's native1318tabs. Avoid `@react-navigation/bottom-tabs` when native feel matters.13191320### Stack Navigation13211322**Incorrect (JS stack navigator):**13231324```tsx1325import { createStackNavigator } from '@react-navigation/stack'13261327const Stack = createStackNavigator()13281329function App() {1330 return (1331 <Stack.Navigator>1332 <Stack.Screen name='Home' component={HomeScreen} />1333 <Stack.Screen name='Details' component={DetailsScreen} />1334 </Stack.Navigator>1335 )1336}1337```13381339**Correct (native stack with react-navigation):**13401341```tsx1342import { createNativeStackNavigator } from '@react-navigation/native-stack'13431344const Stack = createNativeStackNavigator()13451346function App() {1347 return (1348 <Stack.Navigator>1349 <Stack.Screen name='Home' component={HomeScreen} />1350 <Stack.Screen name='Details' component={DetailsScreen} />1351 </Stack.Navigator>1352 )1353}1354```13551356**Correct (expo-router uses native stack by default):**13571358```tsx1359// app/_layout.tsx1360import { Stack } from 'expo-router'13611362export default function Layout() {1363 return <Stack />1364}1365```13661367### Tab Navigation13681369**Incorrect (JS bottom tabs):**13701371```tsx1372import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'13731374const Tab = createBottomTabNavigator()13751376function App() {1377 return (1378 <Tab.Navigator>1379 <Tab.Screen name='Home' component={HomeScreen} />1380 <Tab.Screen name='Settings' component={SettingsScreen} />1381 </Tab.Navigator>1382 )1383}1384```13851386**Correct (native bottom tabs with react-navigation):**13871388```tsx1389import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation'13901391const Tab = createNativeBottomTabNavigator()13921393function App() {1394 return (1395 <Tab.Navigator>1396 <Tab.Screen1397 name='Home'1398 component={HomeScreen}1399 options={{1400 tabBarIcon: () => ({ sfSymbol: 'house' }),1401 }}1402 />1403 <Tab.Screen1404 name='Settings'1405 component={SettingsScreen}1406 options={{1407 tabBarIcon: () => ({ sfSymbol: 'gear' }),1408 }}1409 />1410 </Tab.Navigator>1411 )1412}1413```14141415**Correct (expo-router native tabs):**14161417```tsx1418// app/(tabs)/_layout.tsx1419import { NativeTabs } from 'expo-router/unstable-native-tabs'14201421export default function TabLayout() {1422 return (1423 <NativeTabs>1424 <NativeTabs.Trigger name='index'>1425 <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>1426 <NativeTabs.Trigger.Icon sf='house.fill' md='home' />1427 </NativeTabs.Trigger>1428 <NativeTabs.Trigger name='settings'>1429 <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>1430 <NativeTabs.Trigger.Icon sf='gear' md='settings' />1431 </NativeTabs.Trigger>1432 </NativeTabs>1433 )1434}1435```14361437On iOS, native tabs automatically enable `contentInsetAdjustmentBehavior` on the1438first `ScrollView` at the root of each tab screen, so content scrolls correctly1439behind the translucent tab bar. If you need to disable this, use1440`disableAutomaticContentInsets` on the trigger.14411442### Prefer Native Header Options Over Custom Components14431444**Incorrect (custom header component):**14451446```tsx1447<Stack.Screen1448 name='Profile'1449 component={ProfileScreen}1450 options={{1451 header: () => <CustomHeader title='Profile' />,1452 }}1453/>1454```14551456**Correct (native header options):**14571458```tsx1459<Stack.Screen1460 name='Profile'1461 component={ProfileScreen}1462 options={{1463 title: 'Profile',1464 headerLargeTitleEnabled: true,1465 headerSearchBarOptions: {1466 placeholder: 'Search',1467 },1468 }}1469/>1470```14711472Native headers support iOS large titles, search bars, blur effects, and proper1473safe area handling automatically.14741475### Why Native Navigators14761477- **Performance**: Native transitions and gestures run on the UI thread1478- **Platform behavior**: Automatic iOS large titles, Android material design1479- **System integration**: Scroll-to-top on tab tap, PiP avoidance, proper safe1480 areas1481- **Accessibility**: Platform accessibility features work automatically14821483Reference:14841485- [React Navigation Native Stack](https://reactnavigation.org/docs/native-stack-navigator)1486- [React Native Bottom Tabs with React Navigation](https://oss.callstack.com/react-native-bottom-tabs/docs/guides/usage-with-react-navigation)1487- [React Native Bottom Tabs with Expo Router](https://oss.callstack.com/react-native-bottom-tabs/docs/guides/usage-with-expo-router)1488- [Expo Router Native Tabs](https://docs.expo.dev/router/advanced/native-tabs)1489<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:navigation-native-navigators:end -->14901491<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:react-compiler-destructure-functions:start -->1492## Destructure Functions Early in Render14931494This rule is only applicable if you are using the React Compiler.14951496Destructure functions from hooks at the top of render scope. Never dot into1497objects to call functions. Destructured functions are stable references; dotting1498creates new references and breaks memoization.14991500**Incorrect (dotting into object):**15011502```tsx1503import { useRouter } from 'expo-router'15041505function SaveButton(props) {1506 const router = useRouter()15071508 // bad: react-compiler will key the cache on "props" and "router", which are objects that change each render1509 const handlePress = () => {1510 props.onSave()1511 router.push('/success') // unstable reference1512 }15131514 return <Button onPress={handlePress}>Save</Button>1515}1516```15171518**Correct (destructure early):**15191520```tsx1521import { useRouter } from 'expo-router'15221523function SaveButton({ onSave }) {1524 const { push } = useRouter()15251526 // good: react-compiler will key on push and onSave1527 const handlePress = () => {1528 onSave()1529 push('/success') // stable reference1530 }15311532 return <Button onPress={handlePress}>Save</Button>1533}1534```1535<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:react-compiler-destructure-functions:end -->15361537<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:react-compiler-reanimated-shared-values:start -->1538## Use .get() and .set() for Shared Values with React Compiler15391540With React Compiler enabled, use `.get()` and `.set()` instead of reading or1541writing `.value` directly on Reanimated shared values. The compiler can't track1542property access—explicit methods ensure correct behavior.15431544**Incorrect (breaks with React Compiler):**15451546```tsx1547import { useSharedValue } from 'react-native-reanimated'15481549function Counter() {1550 const count = useSharedValue(0)15511552 const increment = () => {1553 count.value = count.value + 1 // opts out of react compiler1554 }15551556 return <Button onPress={increment} title={`Count: ${count.value}`} />1557}1558```15591560**Correct (React Compiler compatible):**15611562```tsx1563import { useSharedValue } from 'react-native-reanimated'15641565function Counter() {1566 const count = useSharedValue(0)15671568 const increment = () => {1569 count.set(count.get() + 1)1570 }15711572 return <Button onPress={increment} title={`Count: ${count.get()}`} />1573}1574```15751576See the1577[Reanimated docs](https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support)1578for more.1579<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:react-compiler-reanimated-shared-values:end -->15801581<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:react-state-dispatcher:start -->1582## Use Dispatch Updaters for State That Depends on Current Value15831584When the next state depends on the current state, use a dispatch updater1585(`setState(prev => ...)`) instead of reading the state variable directly in a1586callback. This avoids stale closures and ensures you're comparing against the1587latest value.15881589**Incorrect (reads state directly):**15901591```tsx1592const [size, setSize] = useState<Size | undefined>(undefined)15931594const onLayout = (e: LayoutChangeEvent) => {1595 const { width, height } = e.nativeEvent.layout1596 // size may be stale in this closure1597 if (size?.width !== width || size?.height !== height) {1598 setSize({ width, height })1599 }1600}1601```16021603**Correct (dispatch updater):**16041605```tsx1606const [size, setSize] = useState<Size | undefined>(undefined)16071608const onLayout = (e: LayoutChangeEvent) => {1609 const { width, height } = e.nativeEvent.layout1610 setSize((prev) => {1611 if (prev?.width === width && prev?.height === height) return prev1612 return { width, height }1613 })1614}1615```16161617Returning the previous value from the updater skips the re-render.16181619For primitive states, you don't need to compare values before firing a1620re-render.16211622**Incorrect (unnecessary comparison for primitive state):**16231624```tsx1625const [size, setSize] = useState<Size | undefined>(undefined)16261627const onLayout = (e: LayoutChangeEvent) => {1628 const { width, height } = e.nativeEvent.layout1629 setSize((prev) => (prev === width ? prev : width))1630}1631```16321633**Correct (sets primitive state directly):**16341635```tsx1636const [size, setSize] = useState<Size | undefined>(undefined)16371638const onLayout = (e: LayoutChangeEvent) => {1639 const { width, height } = e.nativeEvent.layout1640 setSize(width)1641}1642```16431644However, if the next state depends on the current state, you should still use a1645dispatch updater.16461647**Incorrect (reads state directly from the callback):**16481649```tsx1650const [count, setCount] = useState(0)16511652const onTap = () => {1653 setCount(count + 1)1654}1655```16561657**Correct (dispatch updater):**16581659```tsx1660const [count, setCount] = useState(0)16611662const onTap = () => {1663 setCount((prev) => prev + 1)1664}1665```1666<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:react-state-dispatcher:end -->16671668<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:react-state-fallback:start -->1669## Use fallback state instead of initialState16701671Use `undefined` as initial state and nullish coalescing (`??`) to fall back to1672parent or server values. State represents user intent only—`undefined` means1673"user hasn't chosen yet." This enables reactive fallbacks that update when the1674source changes, not just on initial render.16751676**Incorrect (syncs state, loses reactivity):**16771678```tsx1679type Props = { fallbackEnabled: boolean }16801681function Toggle({ fallbackEnabled }: Props) {1682 const [enabled, setEnabled] = useState(defaultEnabled)1683 // If fallbackEnabled changes, state is stale1684 // State mixes user intent with default value16851686 return <Switch value={enabled} onValueChange={setEnabled} />1687}1688```16891690**Correct (state is user intent, reactive fallback):**16911692```tsx1693type Props = { fallbackEnabled: boolean }16941695function Toggle({ fallbackEnabled }: Props) {1696 const [_enabled, setEnabled] = useState<boolean | undefined>(undefined)1697 const enabled = _enabled ?? defaultEnabled1698 // undefined = user hasn't touched it, falls back to prop1699 // If defaultEnabled changes, component reflects it1700 // Once user interacts, their choice persists17011702 return <Switch value={enabled} onValueChange={setEnabled} />1703}1704```17051706**With server data:**17071708```tsx1709function ProfileForm({ data }: { data: User }) {1710 const [_theme, setTheme] = useState<string | undefined>(undefined)1711 const theme = _theme ?? data.theme1712 // Shows server value until user overrides1713 // Server refetch updates the fallback automatically17141715 return <ThemePicker value={theme} onChange={setTheme} />1716}1717```1718<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:react-state-fallback:end -->17191720<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:react-state-minimize:start -->1721## Minimize State Variables and Derive Values17221723Use the fewest state variables possible. If a value can be computed from existing state or props, derive it during render instead of storing it in state. Redundant state causes unnecessary re-renders and can drift out of sync.17241725**Incorrect (redundant state):**17261727```tsx1728function Cart({ items }: { items: Item[] }) {1729 const [total, setTotal] = useState(0)1730 const [itemCount, setItemCount] = useState(0)17311732 useEffect(() => {1733 setTotal(items.reduce((sum, item) => sum + item.price, 0))1734 setItemCount(items.length)1735 }, [items])17361737 return (1738 <View>1739 <Text>{itemCount} items</Text>1740 <Text>Total: ${total}</Text>1741 </View>1742 )1743}1744```17451746**Correct (derived values):**17471748```tsx1749function Cart({ items }: { items: Item[] }) {1750 const total = items.reduce((sum, item) => sum + item.price, 0)1751 const itemCount = items.length17521753 return (1754 <View>1755 <Text>{itemCount} items</Text>1756 <Text>Total: ${total}</Text>1757 </View>1758 )1759}1760```17611762**Another example:**17631764```tsx1765// Incorrect: storing both firstName, lastName, AND fullName1766const [firstName, setFirstName] = useState('')1767const [lastName, setLastName] = useState('')1768const [fullName, setFullName] = useState('')17691770// Correct: derive fullName1771const [firstName, setFirstName] = useState('')1772const [lastName, setLastName] = useState('')1773const fullName = `${firstName} ${lastName}`1774```17751776State should be the minimal source of truth. Everything else is derived.17771778Reference: [Choosing the State Structure](https://react.dev/learn/choosing-the-state-structure)1779<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:react-state-minimize:end -->17801781<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:rendering-no-falsy-and:start -->1782## Never Use && with Potentially Falsy Values17831784Never use `{value && <Component />}` when `value` could be an empty string or1785`0`. These are falsy but JSX-renderable—React Native will try to render them as1786text outside a `<Text>` component, causing a hard crash in production.17871788**Incorrect (crashes if count is 0 or name is ""):**17891790```tsx1791function Profile({ name, count }: { name: string; count: number }) {1792 return (1793 <View>1794 {name && <Text>{name}</Text>}1795 {count && <Text>{count} items</Text>}1796 </View>1797 )1798}1799// If name="" or count=0, renders the falsy value → crash1800```18011802**Correct (ternary with null):**18031804```tsx1805function Profile({ name, count }: { name: string; count: number }) {1806 return (1807 <View>1808 {name ? <Text>{name}</Text> : null}1809 {count ? <Text>{count} items</Text> : null}1810 </View>1811 )1812}1813```18141815**Correct (explicit boolean coercion):**18161817```tsx1818function Profile({ name, count }: { name: string; count: number }) {1819 return (1820 <View>1821 {!!name && <Text>{name}</Text>}1822 {!!count && <Text>{count} items</Text>}1823 </View>1824 )1825}1826```18271828**Best (early return):**18291830```tsx1831function Profile({ name, count }: { name: string; count: number }) {1832 if (!name) return null18331834 return (1835 <View>1836 <Text>{name}</Text>1837 {count > 0 ? <Text>{count} items</Text> : null}1838 </View>1839 )1840}1841```18421843Early returns are clearest. When using conditionals inline, prefer ternary or1844explicit boolean checks.18451846**Lint rule:** Enable `react/jsx-no-leaked-render` from1847[eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-leaked-render.md)1848to catch this automatically.1849<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:rendering-no-falsy-and:end -->18501851<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:rendering-text-in-text-component:start -->1852## Wrap Strings in Text Components18531854Strings must be rendered inside `<Text>`. React Native crashes if a string is a1855direct child of `<View>`.18561857**Incorrect (crashes):**18581859```tsx1860import { View } from 'react-native'18611862function Greeting({ name }: { name: string }) {1863 return <View>Hello, {name}!</View>1864}1865// Error: Text strings must be rendered within a <Text> component.1866```18671868**Correct:**18691870```tsx1871import { View, Text } from 'react-native'18721873function Greeting({ name }: { name: string }) {1874 return (1875 <View>1876 <Text>Hello, {name}!</Text>1877 </View>1878 )1879}1880```1881<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:rendering-text-in-text-component:end -->18821883<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:scroll-position-no-state:start -->1884## Never Track Scroll Position in useState18851886Never store scroll position in `useState`. Scroll events fire rapidly—state1887updates cause render thrashing and dropped frames. Use a Reanimated shared value1888for animations or a ref for non-reactive tracking.18891890**Incorrect (useState causes jank):**18911892```tsx1893import { useState } from 'react'1894import {1895 ScrollView,1896 NativeSyntheticEvent,1897 NativeScrollEvent,1898} from 'react-native'18991900function Feed() {1901 const [scrollY, setScrollY] = useState(0)19021903 const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {1904 setScrollY(e.nativeEvent.contentOffset.y) // re-renders on every frame1905 }19061907 return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />1908}1909```19101911**Correct (Reanimated for animations):**19121913```tsx1914import Animated, {1915 useSharedValue,1916 useAnimatedScrollHandler,1917} from 'react-native-reanimated'19181919function Feed() {1920 const scrollY = useSharedValue(0)19211922 const onScroll = useAnimatedScrollHandler({1923 onScroll: (e) => {1924 scrollY.value = e.contentOffset.y // runs on UI thread, no re-render1925 },1926 })19271928 return (1929 <Animated.ScrollView1930 onScroll={onScroll}1931 // higher number has better performance, but it fires less often.1932 // unset this if you need higher precision over performance.1933 scrollEventThrottle={16}1934 />1935 )1936}1937```19381939**Correct (ref for non-reactive tracking):**19401941```tsx1942import { useRef } from 'react'1943import {1944 ScrollView,1945 NativeSyntheticEvent,1946 NativeScrollEvent,1947} from 'react-native'19481949function Feed() {1950 const scrollY = useRef(0)19511952 const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {1953 scrollY.current = e.nativeEvent.contentOffset.y // no re-render1954 }19551956 return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />1957}1958```1959<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:scroll-position-no-state:end -->19601961<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:state-ground-truth:start -->1962## State Must Represent Ground Truth19631964State variables—both React `useState` and Reanimated shared values—should1965represent the actual state of something (e.g., `pressed`, `progress`, `isOpen`),1966not derived visual values (e.g., `scale`, `opacity`, `translateY`). Derive1967visual values from state using computation or interpolation.19681969**Incorrect (storing the visual output):**19701971```tsx1972const scale = useSharedValue(1)19731974const tap = Gesture.Tap()1975 .onBegin(() => {1976 scale.set(withTiming(0.95))1977 })1978 .onFinalize(() => {1979 scale.set(withTiming(1))1980 })19811982const animatedStyle = useAnimatedStyle(() => ({1983 transform: [{ scale: scale.get() }],1984}))1985```19861987**Correct (storing the state, deriving the visual):**19881989```tsx1990const pressed = useSharedValue(0) // 0 = not pressed, 1 = pressed19911992const tap = Gesture.Tap()1993 .onBegin(() => {1994 pressed.set(withTiming(1))1995 })1996 .onFinalize(() => {1997 pressed.set(withTiming(0))1998 })19992000const animatedStyle = useAnimatedStyle(() => ({2001 transform: [{ scale: interpolate(pressed.get(), [0, 1], [1, 0.95]) }],2002}))2003```20042005**Why this matters:**20062007State variables should represent real "state", not necessarily a desired end2008result.200920101. **Single source of truth** — The state (`pressed`) describes what's2011 happening; visuals are derived20122. **Easier to extend** — Adding opacity, rotation, or other effects just2013 requires more interpolations from the same state20143. **Debugging** — Inspecting `pressed = 1` is clearer than `scale = 0.95`20154. **Reusable logic** — The same `pressed` value can drive multiple visual2016 properties20172018**Same principle for React state:**20192020```tsx2021// Incorrect: storing derived values2022const [isExpanded, setIsExpanded] = useState(false)2023const [height, setHeight] = useState(0)20242025useEffect(() => {2026 setHeight(isExpanded ? 200 : 0)2027}, [isExpanded])20282029// Correct: derive from state2030const [isExpanded, setIsExpanded] = useState(false)2031const height = isExpanded ? 200 : 02032```20332034State is the minimal truth. Everything else is derived.2035<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:state-ground-truth:end -->20362037<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:ui-expo-image:start -->2038## Use expo-image for Optimized Images20392040Use `expo-image` instead of React Native's `Image`. It provides memory-efficient caching, blurhash placeholders, progressive loading, and better performance for lists.20412042**Incorrect (React Native Image):**20432044```tsx2045import { Image } from 'react-native'20462047function Avatar({ url }: { url: string }) {2048 return <Image source={{ uri: url }} style={styles.avatar} />2049}2050```20512052**Correct (expo-image):**20532054```tsx2055import { Image } from 'expo-image'20562057function Avatar({ url }: { url: string }) {2058 return <Image source={{ uri: url }} style={styles.avatar} />2059}2060```20612062**With blurhash placeholder:**20632064```tsx2065<Image2066 source={{ uri: url }}2067 placeholder={{ blurhash: 'LGF5]+Yk^6#M@-5c,1J5@[or[Q6.' }}2068 contentFit="cover"2069 transition={200}2070 style={styles.image}2071/>2072```20732074**With priority and caching:**20752076```tsx2077<Image2078 source={{ uri: url }}2079 priority="high"2080 cachePolicy="memory-disk"2081 style={styles.hero}2082/>2083```20842085**Key props:**20862087- `placeholder` — Blurhash or thumbnail while loading2088- `contentFit` — `cover`, `contain`, `fill`, `scale-down`2089- `transition` — Fade-in duration (ms)2090- `priority` — `low`, `normal`, `high`2091- `cachePolicy` — `memory`, `disk`, `memory-disk`, `none`2092- `recyclingKey` — Unique key for list recycling20932094For cross-platform (web + native), use `SolitoImage` from `solito/image` which uses `expo-image` under the hood.20952096Reference: [expo-image](https://docs.expo.dev/versions/latest/sdk/image/)2097<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:ui-expo-image:end -->20982099<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:ui-image-gallery:start -->2100## Use Galeria for Image Galleries and Lightbox21012102For image galleries with lightbox (tap to fullscreen), use `@nandorojo/galeria`.2103It provides native shared element transitions with pinch-to-zoom, double-tap2104zoom, and pan-to-close. Works with any image component including `expo-image`.21052106**Incorrect (custom modal implementation):**21072108```tsx2109function ImageGallery({ urls }: { urls: string[] }) {2110 const [selected, setSelected] = useState<string | null>(null)21112112 return (2113 <>2114 {urls.map((url) => (2115 <Pressable key={url} onPress={() => setSelected(url)}>2116 <Image source={{ uri: url }} style={styles.thumbnail} />2117 </Pressable>2118 ))}2119 <Modal visible={!!selected} onRequestClose={() => setSelected(null)}>2120 <Image source={{ uri: selected! }} style={styles.fullscreen} />2121 </Modal>2122 </>2123 )2124}2125```21262127**Correct (Galeria with expo-image):**21282129```tsx2130import { Galeria } from '@nandorojo/galeria'2131import { Image } from 'expo-image'21322133function ImageGallery({ urls }: { urls: string[] }) {2134 return (2135 <Galeria urls={urls}>2136 {urls.map((url, index) => (2137 <Galeria.Image index={index} key={url}>2138 <Image source={{ uri: url }} style={styles.thumbnail} />2139 </Galeria.Image>2140 ))}2141 </Galeria>2142 )2143}2144```21452146**Single image:**21472148```tsx2149import { Galeria } from '@nandorojo/galeria'2150import { Image } from 'expo-image'21512152function Avatar({ url }: { url: string }) {2153 return (2154 <Galeria urls={[url]}>2155 <Galeria.Image>2156 <Image source={{ uri: url }} style={styles.avatar} />2157 </Galeria.Image>2158 </Galeria>2159 )2160}2161```21622163**With low-res thumbnails and high-res fullscreen:**21642165```tsx2166<Galeria urls={highResUrls}>2167 {lowResUrls.map((url, index) => (2168 <Galeria.Image index={index} key={url}>2169 <Image source={{ uri: url }} style={styles.thumbnail} />2170 </Galeria.Image>2171 ))}2172</Galeria>2173```21742175**With FlashList:**21762177```tsx2178<Galeria urls={urls}>2179 <FlashList2180 data={urls}2181 renderItem={({ item, index }) => (2182 <Galeria.Image index={index}>2183 <Image source={{ uri: item }} style={styles.thumbnail} />2184 </Galeria.Image>2185 )}2186 numColumns={3}2187 estimatedItemSize={100}2188 />2189</Galeria>2190```21912192Works with `expo-image`, `SolitoImage`, `react-native` Image, or any image2193component.21942195Reference: [Galeria](https://github.com/nandorojo/galeria)2196<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:ui-image-gallery:end -->21972198<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:ui-measure-views:start -->2199## Measuring View Dimensions22002201Use both `useLayoutEffect` (synchronous) and `onLayout` (for updates). The sync2202measurement gives you the initial size immediately; `onLayout` keeps it current2203when the view changes. For non-primitive states, use a dispatch updater to2204compare values and avoid unnecessary re-renders.22052206**Height only:**22072208```tsx2209import { useLayoutEffect, useRef, useState } from 'react'2210import { View, LayoutChangeEvent } from 'react-native'22112212function MeasuredBox({ children }: { children: React.ReactNode }) {2213 const ref = useRef<View>(null)2214 const [height, setHeight] = useState<number | undefined>(undefined)22152216 useLayoutEffect(() => {2217 // Sync measurement on mount (RN 0.82+)2218 const rect = ref.current?.getBoundingClientRect()2219 if (rect) setHeight(rect.height)2220 // Pre-0.82: ref.current?.measure((x, y, w, h) => setHeight(h))2221 }, [])22222223 const onLayout = (e: LayoutChangeEvent) => {2224 setHeight(e.nativeEvent.layout.height)2225 }22262227 return (2228 <View ref={ref} onLayout={onLayout}>2229 {children}2230 </View>2231 )2232}2233```22342235**Both dimensions:**22362237```tsx2238import { useLayoutEffect, useRef, useState } from 'react'2239import { View, LayoutChangeEvent } from 'react-native'22402241type Size = { width: number; height: number }22422243function MeasuredBox({ children }: { children: React.ReactNode }) {2244 const ref = useRef<View>(null)2245 const [size, setSize] = useState<Size | undefined>(undefined)22462247 useLayoutEffect(() => {2248 const rect = ref.current?.getBoundingClientRect()2249 if (rect) setSize({ width: rect.width, height: rect.height })2250 }, [])22512252 const onLayout = (e: LayoutChangeEvent) => {2253 const { width, height } = e.nativeEvent.layout2254 setSize((prev) => {2255 // for non-primitive states, compare values before firing a re-render2256 if (prev?.width === width && prev?.height === height) return prev2257 return { width, height }2258 })2259 }22602261 return (2262 <View ref={ref} onLayout={onLayout}>2263 {children}2264 </View>2265 )2266}2267```22682269Use functional setState to compare—don't read state directly in the callback.2270<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:ui-measure-views:end -->22712272<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:ui-menus:start -->2273## Use Native Menus for Dropdowns and Context Menus22742275Use native platform menus instead of custom JS implementations. Native menus2276provide built-in accessibility, consistent platform UX, and better performance.2277Use [zeego](https://zeego.dev) for cross-platform native menus.22782279**Incorrect (custom JS menu):**22802281```tsx2282import { useState } from 'react'2283import { View, Pressable, Text } from 'react-native'22842285function MyMenu() {2286 const [open, setOpen] = useState(false)22872288 return (2289 <View>2290 <Pressable onPress={() => setOpen(!open)}>2291 <Text>Open Menu</Text>2292 </Pressable>2293 {open && (2294 <View style={{ position: 'absolute', top: 40 }}>2295 <Pressable onPress={() => console.log('edit')}>2296 <Text>Edit</Text>2297 </Pressable>2298 <Pressable onPress={() => console.log('delete')}>2299 <Text>Delete</Text>2300 </Pressable>2301 </View>2302 )}2303 </View>2304 )2305}2306```23072308**Correct (native menu with zeego):**23092310```tsx2311import * as DropdownMenu from 'zeego/dropdown-menu'23122313function MyMenu() {2314 return (2315 <DropdownMenu.Root>2316 <DropdownMenu.Trigger>2317 <Pressable>2318 <Text>Open Menu</Text>2319 </Pressable>2320 </DropdownMenu.Trigger>23212322 <DropdownMenu.Content>2323 <DropdownMenu.Item key='edit' onSelect={() => console.log('edit')}>2324 <DropdownMenu.ItemTitle>Edit</DropdownMenu.ItemTitle>2325 </DropdownMenu.Item>23262327 <DropdownMenu.Item2328 key='delete'2329 destructive2330 onSelect={() => console.log('delete')}2331 >2332 <DropdownMenu.ItemTitle>Delete</DropdownMenu.ItemTitle>2333 </DropdownMenu.Item>2334 </DropdownMenu.Content>2335 </DropdownMenu.Root>2336 )2337}2338```23392340**Context menu (long-press):**23412342```tsx2343import * as ContextMenu from 'zeego/context-menu'23442345function MyContextMenu() {2346 return (2347 <ContextMenu.Root>2348 <ContextMenu.Trigger>2349 <View style={{ padding: 20 }}>2350 <Text>Long press me</Text>2351 </View>2352 </ContextMenu.Trigger>23532354 <ContextMenu.Content>2355 <ContextMenu.Item key='copy' onSelect={() => console.log('copy')}>2356 <ContextMenu.ItemTitle>Copy</ContextMenu.ItemTitle>2357 </ContextMenu.Item>23582359 <ContextMenu.Item key='paste' onSelect={() => console.log('paste')}>2360 <ContextMenu.ItemTitle>Paste</ContextMenu.ItemTitle>2361 </ContextMenu.Item>2362 </ContextMenu.Content>2363 </ContextMenu.Root>2364 )2365}2366```23672368**Checkbox items:**23692370```tsx2371import * as DropdownMenu from 'zeego/dropdown-menu'23722373function SettingsMenu() {2374 const [notifications, setNotifications] = useState(true)23752376 return (2377 <DropdownMenu.Root>2378 <DropdownMenu.Trigger>2379 <Pressable>2380 <Text>Settings</Text>2381 </Pressable>2382 </DropdownMenu.Trigger>23832384 <DropdownMenu.Content>2385 <DropdownMenu.CheckboxItem2386 key='notifications'2387 value={notifications}2388 onValueChange={() => setNotifications((prev) => !prev)}2389 >2390 <DropdownMenu.ItemIndicator />2391 <DropdownMenu.ItemTitle>Notifications</DropdownMenu.ItemTitle>2392 </DropdownMenu.CheckboxItem>2393 </DropdownMenu.Content>2394 </DropdownMenu.Root>2395 )2396}2397```23982399**Submenus:**24002401```tsx2402import * as DropdownMenu from 'zeego/dropdown-menu'24032404function MenuWithSubmenu() {2405 return (2406 <DropdownMenu.Root>2407 <DropdownMenu.Trigger>2408 <Pressable>2409 <Text>Options</Text>2410 </Pressable>2411 </DropdownMenu.Trigger>24122413 <DropdownMenu.Content>2414 <DropdownMenu.Item key='home' onSelect={() => console.log('home')}>2415 <DropdownMenu.ItemTitle>Home</DropdownMenu.ItemTitle>2416 </DropdownMenu.Item>24172418 <DropdownMenu.Sub>2419 <DropdownMenu.SubTrigger key='more'>2420 <DropdownMenu.ItemTitle>More Options</DropdownMenu.ItemTitle>2421 </DropdownMenu.SubTrigger>24222423 <DropdownMenu.SubContent>2424 <DropdownMenu.Item key='settings'>2425 <DropdownMenu.ItemTitle>Settings</DropdownMenu.ItemTitle>2426 </DropdownMenu.Item>24272428 <DropdownMenu.Item key='help'>2429 <DropdownMenu.ItemTitle>Help</DropdownMenu.ItemTitle>2430 </DropdownMenu.Item>2431 </DropdownMenu.SubContent>2432 </DropdownMenu.Sub>2433 </DropdownMenu.Content>2434 </DropdownMenu.Root>2435 )2436}2437```24382439Reference: [Zeego Documentation](https://zeego.dev/components/dropdown-menu)2440<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:ui-menus:end -->24412442<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:ui-native-modals:start -->2443## Use Native Modals Over JS-Based Bottom Sheets24442445Use native `<Modal>` with `presentationStyle="formSheet"` or React Navigation2446v7's native form sheet instead of JS-based bottom sheet libraries. Native modals2447have built-in gestures, accessibility, and better performance. Rely on native UI2448for low-level primitives.24492450**Incorrect (JS-based bottom sheet):**24512452```tsx2453import BottomSheet from 'custom-js-bottom-sheet'24542455function MyScreen() {2456 const sheetRef = useRef<BottomSheet>(null)24572458 return (2459 <View style={{ flex: 1 }}>2460 <Button onPress={() => sheetRef.current?.expand()} title='Open' />2461 <BottomSheet ref={sheetRef} snapPoints={['50%', '90%']}>2462 <View>2463 <Text>Sheet content</Text>2464 </View>2465 </BottomSheet>2466 </View>2467 )2468}2469```24702471**Correct (native Modal with formSheet):**24722473```tsx2474import { Modal, View, Text, Button } from 'react-native'24752476function MyScreen() {2477 const [visible, setVisible] = useState(false)24782479 return (2480 <View style={{ flex: 1 }}>2481 <Button onPress={() => setVisible(true)} title='Open' />2482 <Modal2483 visible={visible}2484 presentationStyle='formSheet'2485 animationType='slide'2486 onRequestClose={() => setVisible(false)}2487 >2488 <View>2489 <Text>Sheet content</Text>2490 </View>2491 </Modal>2492 </View>2493 )2494}2495```24962497**Correct (React Navigation v7 native form sheet):**24982499```tsx2500// In your navigator2501<Stack.Screen2502 name='Details'2503 component={DetailsScreen}2504 options={{2505 presentation: 'formSheet',2506 sheetAllowedDetents: 'fitToContents',2507 }}2508/>2509```25102511Native modals provide swipe-to-dismiss, proper keyboard avoidance, and2512accessibility out of the box.2513<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:ui-native-modals:end -->25142515<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:ui-pressable:start -->2516## Use Pressable Instead of Touchable Components25172518Never use `TouchableOpacity` or `TouchableHighlight`. Use `Pressable` from2519`react-native` or `react-native-gesture-handler` instead.25202521**Incorrect (legacy Touchable components):**25222523```tsx2524import { TouchableOpacity } from 'react-native'25252526function MyButton({ onPress }: { onPress: () => void }) {2527 return (2528 <TouchableOpacity onPress={onPress} activeOpacity={0.7}>2529 <Text>Press me</Text>2530 </TouchableOpacity>2531 )2532}2533```25342535**Correct (Pressable):**25362537```tsx2538import { Pressable } from 'react-native'25392540function MyButton({ onPress }: { onPress: () => void }) {2541 return (2542 <Pressable onPress={onPress}>2543 <Text>Press me</Text>2544 </Pressable>2545 )2546}2547```25482549**Correct (Pressable from gesture handler for lists):**25502551```tsx2552import { Pressable } from 'react-native-gesture-handler'25532554function ListItem({ onPress }: { onPress: () => void }) {2555 return (2556 <Pressable onPress={onPress}>2557 <Text>Item</Text>2558 </Pressable>2559 )2560}2561```25622563Use `react-native-gesture-handler` Pressable inside scrollable lists for better2564gesture coordination, as long as you are using the ScrollView from2565`react-native-gesture-handler` as well.25662567**For animated press states (scale, opacity changes):** Use `GestureDetector`2568with Reanimated shared values instead of Pressable's style callback. See the2569`animation-gesture-detector-press` rule.2570<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:ui-pressable:end -->25712572<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:ui-safe-area-scroll:start -->2573## Use contentInsetAdjustmentBehavior for Safe Areas25742575Use `contentInsetAdjustmentBehavior="automatic"` on the root ScrollView instead of wrapping content in SafeAreaView or manual padding. This lets iOS handle safe area insets natively with proper scroll behavior.25762577**Incorrect (SafeAreaView wrapper):**25782579```tsx2580import { SafeAreaView, ScrollView, View, Text } from 'react-native'25812582function MyScreen() {2583 return (2584 <SafeAreaView style={{ flex: 1 }}>2585 <ScrollView>2586 <View>2587 <Text>Content</Text>2588 </View>2589 </ScrollView>2590 </SafeAreaView>2591 )2592}2593```25942595**Incorrect (manual safe area padding):**25962597```tsx2598import { ScrollView, View, Text } from 'react-native'2599import { useSafeAreaInsets } from 'react-native-safe-area-context'26002601function MyScreen() {2602 const insets = useSafeAreaInsets()26032604 return (2605 <ScrollView contentContainerStyle={{ paddingTop: insets.top }}>2606 <View>2607 <Text>Content</Text>2608 </View>2609 </ScrollView>2610 )2611}2612```26132614**Correct (native content inset adjustment):**26152616```tsx2617import { ScrollView, View, Text } from 'react-native'26182619function MyScreen() {2620 return (2621 <ScrollView contentInsetAdjustmentBehavior='automatic'>2622 <View>2623 <Text>Content</Text>2624 </View>2625 </ScrollView>2626 )2627}2628```26292630The native approach handles dynamic safe areas (keyboard, toolbars) and allows content to scroll behind the status bar naturally.2631<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:ui-safe-area-scroll:end -->26322633<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:ui-scrollview-content-inset:start -->2634## Use contentInset for Dynamic ScrollView Spacing26352636When adding space to the top or bottom of a ScrollView that may change2637(keyboard, toolbars, dynamic content), use `contentInset` instead of padding.2638Changing `contentInset` doesn't trigger layout recalculation—it adjusts the2639scroll area without re-rendering content.26402641**Incorrect (padding causes layout recalculation):**26422643```tsx2644function Feed({ bottomOffset }: { bottomOffset: number }) {2645 return (2646 <ScrollView contentContainerStyle={{ paddingBottom: bottomOffset }}>2647 {children}2648 </ScrollView>2649 )2650}2651// Changing bottomOffset triggers full layout recalculation2652```26532654**Correct (contentInset for dynamic spacing):**26552656```tsx2657function Feed({ bottomOffset }: { bottomOffset: number }) {2658 return (2659 <ScrollView2660 contentInset={{ bottom: bottomOffset }}2661 scrollIndicatorInsets={{ bottom: bottomOffset }}2662 >2663 {children}2664 </ScrollView>2665 )2666}2667// Changing bottomOffset only adjusts scroll bounds2668```26692670Use `scrollIndicatorInsets` alongside `contentInset` to keep the scroll2671indicator aligned. For static spacing that never changes, padding is fine.2672<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:ui-scrollview-content-inset:end -->26732674<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:ui-styling:start -->2675## Modern React Native Styling Patterns26762677Follow these styling patterns for cleaner, more consistent React Native code.26782679**Always use `borderCurve: 'continuous'` with `borderRadius`:**26802681```tsx2682// Incorrect2683{ borderRadius: 12 }26842685// Correct – smoother iOS-style corners2686{ borderRadius: 12, borderCurve: 'continuous' }2687```26882689**Use `gap` instead of margin for spacing between elements:**26902691```tsx2692// Incorrect – margin on children2693<View>2694 <Text style={{ marginBottom: 8 }}>Title</Text>2695 <Text style={{ marginBottom: 8 }}>Subtitle</Text>2696</View>26972698// Correct – gap on parent2699<View style={{ gap: 8 }}>2700 <Text>Title</Text>2701 <Text>Subtitle</Text>2702</View>2703```27042705**Use `padding` for space within, `gap` for space between:**27062707```tsx2708<View style={{ padding: 16, gap: 12 }}>2709 <Text>First</Text>2710 <Text>Second</Text>2711</View>2712```27132714**Use `experimental_backgroundImage` for linear gradients:**27152716```tsx2717// Incorrect – third-party gradient library2718<LinearGradient colors={['#000', '#fff']} />27192720// Correct – native CSS gradient syntax2721<View2722 style={{2723 experimental_backgroundImage: 'linear-gradient(to bottom, #000, #fff)',2724 }}2725/>2726```27272728**Use CSS `boxShadow` string syntax for shadows:**27292730```tsx2731// Incorrect – legacy shadow objects or elevation2732{ shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1 }2733{ elevation: 4 }27342735// Correct – CSS box-shadow syntax2736{ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' }2737```27382739**Avoid multiple font sizes – use weight and color for emphasis:**27402741```tsx2742// Incorrect – varying font sizes for hierarchy2743<Text style={{ fontSize: 18 }}>Title</Text>2744<Text style={{ fontSize: 14 }}>Subtitle</Text>2745<Text style={{ fontSize: 12 }}>Caption</Text>27462747// Correct – consistent size, vary weight and color2748<Text style={{ fontWeight: '600' }}>Title</Text>2749<Text style={{ color: '#666' }}>Subtitle</Text>2750<Text style={{ color: '#999' }}>Caption</Text>2751```27522753Limiting font sizes creates visual consistency. Use `fontWeight` (bold/semibold)2754and grayscale colors for hierarchy instead.2755<!-- forgecat:@forgecat/vercel-labs_agent-skills_react-native-skills:ui-styling:end -->2756
One repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-build.mdc · 51 | Cursor rules | no sections | 16/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-code-simplify.mdc · 51 | Cursor rules | testing-strategy | 30/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-plan.mdc · 51 | Cursor rules | no sections | 16/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-review.mdc · 51 | Cursor rules | no sections | 16/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-ship.mdc · 51 | Cursor rules | testing-strategygitdeploymentdo-not | 61/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-spec.mdc · 51 | Cursor rules | no sections | 16/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-test.mdc · 51 | Cursor rules | no sections | 16/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-forgecat/AGENTS.md · 51 | AGENTS.md | lint-formatstylearchdo-not | 73/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-forgecat/CLAUDE.md · 51 | CLAUDE.md | teststylearchagent-behaviour | 70/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-code/anthropics_claude-code_ralph-wiggum/for-cursor/.cursor/rules/cmd-cancel-ralph.mdc · 51 | Cursor rules | no sections | 16/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-code/anthropics_claude-code_ralph-wiggum/for-cursor/.cursor/rules/cmd-help.mdc · 51 | Cursor rules | no sections | 54/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-code/anthropics_claude-code_ralph-wiggum/for-cursor/.cursor/rules/cmd-ralph-loop.mdc · 51 | Cursor rules | no sections | 22/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_agent-sdk-dev/for-cursor/.cursor/rules/cmd-new-sdk-app.mdc · 51 | Cursor rules | setupstylearchdocs | 76/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_claude-md-management/for-cursor/.cursor/rules/cmd-revise-claude-md.mdc · 51 | Cursor rules | agent-behaviour | 50/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_code-review/for-cursor/.cursor/rules/cmd-code-review.mdc · 51 | Cursor rules | testing-strategygit | 35/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_commit-commands/for-cursor/.cursor/rules/cmd-clean_gone.mdc · 51 | Cursor rules | no sections | 60/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_commit-commands/for-cursor/.cursor/rules/cmd-commit-push-pr.mdc · 51 | Cursor rules | stylegit | 44/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_commit-commands/for-cursor/.cursor/rules/cmd-commit.mdc · 51 | Cursor rules | style | 44/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_example-plugin/for-cursor/.cursor/rules/cmd-example-command.mdc · 51 | Cursor rules | lint-formatstyleagent-behaviour | 58/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_feature-dev/for-cursor/.cursor/rules/cmd-feature-dev.mdc · 51 | Cursor rules | stylearchgit | 56/100 | today |
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| n8n-io/n8npackages/@n8n/agents/AGENTS.md · 201k | AGENTS.md | buildteststylearch+3 | 100/100 | 14 days ago | |
| aaif-goose/gooseAGENTS.md · 53k | AGENTS.md | setupbuildtestlint-format+7 | 100/100 | 8 days ago | |
| duckduckgo/content-scope-scriptsspecial-pages/AGENTS.md · 70 | AGENTS.md | buildteststylearch+3 | 100/100 | 14 days ago | |
| deepseek-ai/deepseek-harnessnative/landlock-run/AGENTS.md · 104k | AGENTS.md | setupteststylearch+3 | 100/100 | today | |
| code-yeongyu/oh-my-openagentpackages/web/AGENTS.md · 68k | AGENTS.md | setupbuildtestlint-format+6 | 100/100 | 13 days ago | |
| TryGhost/Ghoste2e/AGENTS.md · 55k | AGENTS.md | setupteststylearch+2 | 100/100 | today | |
| vllm-project/vllmAGENTS.md · 89k | AGENTS.md | setuptestlint-formatstyle+5 | 100/100 | 14 days ago | |
| mui/material-uiAGENTS.md · 99k | AGENTS.md | setupbuildtestlint-format+9 | 100/100 | 14 days ago |
A badge carrying the measured quality of the strongest agent config file in this repository, out of 100. It reads from this index every time somebody loads your page, so it changes when the measurement changes and there is nothing to keep up to date. Free, no account, and the value is not something you or we can set by hand.
[](https://rulestack.kynth.studio/configs/nota-america-forgecat-agent-profiles-profiles-vercel-labs-agent-skills-vercel-labs-agent-skills-react-native-skills-for-codex-agents)Would rather not hotlink us? Every badge is also served in shields.io’s endpoint schema, so shields renders the image and your readers never talk to our domain:
Published by Toolproof, the masthead over this index and eight others. The method behind the number is at toolproof.kynth.studio/methodology, and the whole thing is readable as JSON with no key at /api.