Claude Cursor Skill

vercel-react-native-skills

Imported from fcakyon/claude-codex-settings/plugins/react-skills/skills/react-native-skills.

LLM Mart · 0 points · 8 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download fcakyon-claude-codex-settings-plugins_react-skills_skills_react-native-skills-4632eb3.zip · 34 KB
Part of fcakyon/claude-codex-settings — 83 skills

Install

skills CLI npx skills add https://github.com/fcakyon/claude-codex-settings/tree/main/plugins/react-skills/skills/react-native-skills
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install fcakyon-claude-codex-settings@llmmart
Git git clone https://github.com/fcakyon/claude-codex-settings.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole fcakyon/claude-codex-settings collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

React Native Skills

Comprehensive best practices for React Native and Expo applications. Contains rules across multiple categories covering performance, animations, UI patterns, and platform-specific optimizations.

When to Apply

Reference these guidelines when:

  • Building React Native or Expo apps
  • Optimizing list and scroll performance
  • Implementing animations with Reanimated
  • Working with images and media
  • Configuring native modules or fonts
  • Structuring monorepo projects with native dependencies

Rule Categories by Priority

Priority Category Impact Prefix
1 List Performance CRITICAL list-performance-
2 Animation HIGH animation-
3 Navigation HIGH navigation-
4 UI Patterns HIGH ui-
5 State Management MEDIUM react-state-
6 Rendering MEDIUM rendering-
7 Monorepo MEDIUM monorepo-
8 Configuration LOW fonts-, imports-

Quick Reference

1. List Performance (CRITICAL)

  • list-performance-virtualize - Use FlashList for large lists
  • list-performance-item-memo - Memoize list item components
  • list-performance-callbacks - Stabilize callback references
  • list-performance-inline-objects - Avoid inline style objects
  • list-performance-function-references - Extract functions outside render
  • list-performance-images - Optimize images in lists
  • list-performance-item-expensive - Move expensive work outside items
  • list-performance-item-types - Use item types for heterogeneous lists

2. Animation (HIGH)

  • animation-gpu-properties - Animate only transform and opacity
  • animation-derived-value - Use useDerivedValue for computed animations
  • animation-gesture-detector-press - Use Gesture.Tap instead of Pressable

3. Navigation (HIGH)

  • navigation-native-navigators - Use native stack and native tabs over JS navigators

4. UI Patterns (HIGH)

  • ui-expo-image - Use expo-image for all images
  • ui-image-gallery - Use Galeria for image lightboxes
  • ui-pressable - Use Pressable over TouchableOpacity
  • ui-safe-area-scroll - Handle safe areas in ScrollViews
  • ui-scrollview-content-inset - Use contentInset for headers
  • ui-menus - Use native context menus
  • ui-native-modals - Use native modals when possible
  • ui-measure-views - Use onLayout, not measure()
  • ui-styling - Use StyleSheet.create or Nativewind

5. State Management (MEDIUM)

  • react-state-minimize - Minimize state subscriptions
  • react-state-dispatcher - Use dispatcher pattern for callbacks
  • react-state-fallback - Show fallback on first render
  • react-compiler-destructure-functions - Destructure for React Compiler
  • react-compiler-reanimated-shared-values - Handle shared values with compiler

6. Rendering (MEDIUM)

  • rendering-text-in-text-component - Wrap text in Text components
  • rendering-no-falsy-and - Avoid falsy && for conditional rendering

7. Monorepo (MEDIUM)

  • monorepo-native-deps-in-app - Keep native dependencies in app package
  • monorepo-single-dependency-versions - Use single versions across packages

8. Configuration (LOW)

  • fonts-config-plugin - Use config plugins for custom fonts
  • imports-design-system-folder - Organize design system imports
  • js-hoist-intl - Hoist Intl object creation

How to Use

Read individual rule files for detailed explanations and code examples:

rules/list-performance-virtualize.md
rules/animation-gpu-properties.md

Each rule file contains:

  • Brief explanation of why it matters
  • Incorrect code example with explanation
  • Correct code example with explanation
  • Additional context and references

Full Compiled Document

For the complete guide with all rules expanded: AGENTS.md

Files (claude-codex-settings)
  • rules
    • animation-derived-value.md 1.3 KB
      ---
      title: Prefer useDerivedValue Over useAnimatedReaction
      impact: MEDIUM
      impactDescription: cleaner code, automatic dependency tracking
      tags: animation, reanimated, derived-value
      ---
      
      ## Prefer useDerivedValue Over useAnimatedReaction
      
      When deriving a shared value from another, use `useDerivedValue` instead of
      `useAnimatedReaction`. Derived values are declarative, automatically track
      dependencies, and return a value you can use directly. Animated reactions are
      for side effects, not derivations.
      
      **Incorrect (useAnimatedReaction for derivation):**
      
      ```tsx
      import { useSharedValue, useAnimatedReaction } from 'react-native-reanimated'
      
      function MyComponent() {
        const progress = useSharedValue(0)
        const opacity = useSharedValue(1)
      
        useAnimatedReaction(
          () => progress.value,
          (current) => {
            opacity.value = 1 - current
          }
        )
      
        // ...
      }
      ```
      
      **Correct (useDerivedValue):**
      
      ```tsx
      import { useSharedValue, useDerivedValue } from 'react-native-reanimated'
      
      function MyComponent() {
        const progress = useSharedValue(0)
      
        const opacity = useDerivedValue(() => 1 - progress.get())
      
        // ...
      }
      ```
      
      Use `useAnimatedReaction` only for side effects that don't produce a value
      (e.g., triggering haptics, logging, calling `runOnJS`).
      
      Reference:
      [Reanimated useDerivedValue](https://docs.swmansion.com/react-native-reanimated/docs/core/useDerivedValue)
      
    • animation-gesture-detector-press.md 2.5 KB
      ---
      title: Use GestureDetector for Animated Press States
      impact: MEDIUM
      impactDescription: UI thread animations, smoother press feedback
      tags: animation, gestures, press, reanimated
      ---
      
      ## Use GestureDetector for Animated Press States
      
      For animated press states (scale, opacity on press), use `GestureDetector` with
      `Gesture.Tap()` and shared values instead of Pressable's
      `onPressIn`/`onPressOut`. Gesture callbacks run on the UI thread as worklets—no
      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'
      
      function AnimatedButton({ onPress }: { onPress: () => void }) {
        const scale = useSharedValue(1)
      
        const animatedStyle = useAnimatedStyle(() => ({
          transform: [{ scale: scale.value }],
        }))
      
        return (
          <Pressable
            onPress={onPress}
            onPressIn={() => (scale.value = withTiming(0.95))}
            onPressOut={() => (scale.value = withTiming(1))}
          >
            <Animated.View style={animatedStyle}>
              <Text>Press me</Text>
            </Animated.View>
          </Pressable>
        )
      }
      ```
      
      **Correct (GestureDetector with UI thread worklets):**
      
      ```tsx
      import { Gesture, GestureDetector } from 'react-native-gesture-handler'
      import Animated, {
        useSharedValue,
        useAnimatedStyle,
        withTiming,
        interpolate,
        runOnJS,
      } from 'react-native-reanimated'
      
      function AnimatedButton({ onPress }: { onPress: () => void }) {
        // Store the press STATE (0 = not pressed, 1 = pressed)
        const pressed = useSharedValue(0)
      
        const tap = Gesture.Tap()
          .onBegin(() => {
            pressed.set(withTiming(1))
          })
          .onFinalize(() => {
            pressed.set(withTiming(0))
          })
          .onEnd(() => {
            runOnJS(onPress)()
          })
      
        // Derive visual values from the state
        const animatedStyle = useAnimatedStyle(() => ({
          transform: [
            { scale: interpolate(withTiming(pressed.get()), [0, 1], [1, 0.95]) },
          ],
        }))
      
        return (
          <GestureDetector gesture={tap}>
            <Animated.View style={animatedStyle}>
              <Text>Press me</Text>
            </Animated.View>
          </GestureDetector>
        )
      }
      ```
      
      Store the press **state** (0 or 1), then derive the scale via `interpolate`.
      This keeps the shared value as ground truth. Use `runOnJS` to call JS functions
      from worklets. Use `.set()` and `.get()` for React Compiler compatibility.
      
      Reference:
      [Gesture Handler Tap Gesture](https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture)
      
    • animation-gpu-properties.md 2 KB
      ---
      title: Animate Transform and Opacity Instead of Layout Properties
      impact: HIGH
      impactDescription: GPU-accelerated animations, no layout recalculation
      tags: animation, performance, reanimated, transform, opacity
      ---
      
      ## Animate Transform and Opacity Instead of Layout Properties
      
      Avoid 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.
      
      **Incorrect (animates height, triggers layout every frame):**
      
      ```tsx
      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>
      }
      ```
      
      **Correct (animates scaleY, GPU-accelerated):**
      
      ```tsx
      import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
      
      function CollapsiblePanel({ expanded }: { expanded: boolean }) {
        const animatedStyle = useAnimatedStyle(() => ({
          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'
      
      function SlideIn({ visible }: { visible: boolean }) {
        const animatedStyle = useAnimatedStyle(() => ({
          transform: [
            { translateY: withTiming(visible ? 0 : 100) },
          ],
          opacity: withTiming(visible ? 1 : 0),
        }))
      
        return <Animated.View style={animatedStyle}>{children}</Animated.View>
      }
      ```
      
      GPU-accelerated properties: `transform` (translate, scale, rotate), `opacity`. Everything else triggers layout.
      
    • design-system-compound-components.md 1.6 KB
      ---
      title: Use Compound Components Over Polymorphic Children
      impact: MEDIUM
      impactDescription: flexible composition, clearer API
      tags: design-system, components, composition
      ---
      
      ## Use Compound Components Over Polymorphic Children
      
      Don't create components that can accept a string if they aren't a text node. If
      a component can receive a string child, it must be a dedicated `*Text`
      component. For components like buttons, which can have both a View (or
      Pressable) together with text, use compound components, such a `Button`,
      `ButtonText`, and `ButtonIcon`.
      
      **Incorrect (polymorphic children):**
      
      ```tsx
      import { Pressable, Text } from 'react-native'
      
      type ButtonProps = {
        children: string | React.ReactNode
        icon?: React.ReactNode
      }
      
      function Button({ children, icon }: ButtonProps) {
        return (
          <Pressable>
            {icon}
            {typeof children === 'string' ? <Text>{children}</Text> : children}
          </Pressable>
        )
      }
      
      // Usage is ambiguous
      <Button icon={<Icon />}>Save</Button>
      <Button><CustomText>Save</CustomText></Button>
      ```
      
      **Correct (compound components):**
      
      ```tsx
      import { Pressable, Text } from 'react-native'
      
      function Button({ children }: { children: React.ReactNode }) {
        return <Pressable>{children}</Pressable>
      }
      
      function ButtonText({ children }: { children: React.ReactNode }) {
        return <Text>{children}</Text>
      }
      
      function ButtonIcon({ children }: { children: React.ReactNode }) {
        return <>{children}</>
      }
      
      // Usage is explicit and composable
      <Button>
        <ButtonIcon><SaveIcon /></ButtonIcon>
        <ButtonText>Save</ButtonText>
      </Button>
      
      <Button>
        <ButtonText>Cancel</ButtonText>
      </Button>
      ```
      
    • fonts-config-plugin.md 1.4 KB
      ---
      title: Load fonts natively at build time
      impact: LOW
      impactDescription: fonts available at launch, no async loading
      tags: fonts, expo, performance, config-plugin
      ---
      
      ## Use Expo Config Plugin for Font Loading
      
      Use the `expo-font` config plugin to embed fonts at build time instead of
      `useFonts` or `Font.loadAsync`. Embedded fonts are more efficient.
      
      **Incorrect (async font loading):**
      
      ```tsx
      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 (
          <View>
            <Text style={{ fontFamily: 'Geist-Bold' }}>Hello</Text>
          </View>
        )
      }
      ```
      
      **Correct (config plugin, fonts embedded at build):**
      
      ```json
      // app.json
      {
        "expo": {
          "plugins": [
            [
              "expo-font",
              {
                "fonts": ["./assets/fonts/Geist-Bold.otf"]
              }
            ]
          ]
        }
      }
      ```
      
      ```tsx
      import { Text, View } from 'react-native'
      
      function App() {
        // No loading state needed—font is already available
        return (
          <View>
            <Text style={{ fontFamily: 'Geist-Bold' }}>Hello</Text>
          </View>
        )
      }
      ```
      
      After adding fonts to the config plugin, run `npx expo prebuild` and rebuild the
      native app.
      
      Reference:
      [Expo Font Documentation](https://docs.expo.dev/versions/latest/sdk/font/)
      
    • imports-design-system-folder.md 1.4 KB
      ---
      title: Import from Design System Folder
      impact: LOW
      impactDescription: enables global changes and easy refactoring
      tags: imports, architecture, design-system
      ---
      
      ## Import from Design System Folder
      
      Re-export dependencies from a design system folder. App code imports from there,
      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'
      
      function Profile() {
        return (
          <View>
            <Text>Hello</Text>
            <Button>Save</Button>
          </View>
        )
      }
      ```
      
      **Correct (imports from design system):**
      
      ```tsx
      // components/view.tsx
      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} />
      }
      ```
      
      ```tsx
      // components/text.tsx
      export { Text } from 'react-native'
      ```
      
      ```tsx
      // components/button.tsx
      export { Button } from '@ui/button'
      ```
      
      ```tsx
      import { View } from '@/components/view'
      import { Text } from '@/components/text'
      import { Button } from '@/components/button'
      
      function Profile() {
        return (
          <View>
            <Text>Hello</Text>
            <Button>Save</Button>
          </View>
        )
      }
      ```
      
      Start by simply re-exporting. Customize later without changing app code.
      
    • js-hoist-intl.md 1.6 KB
      ---
      title: Hoist Intl Formatter Creation
      impact: LOW-MEDIUM
      impactDescription: avoids expensive object recreation
      tags: javascript, intl, optimization, memoization
      ---
      
      ## Hoist Intl Formatter Creation
      
      Don't create `Intl.DateTimeFormat`, `Intl.NumberFormat`, or
      `Intl.RelativeTimeFormat` inside render or loops. These are expensive to
      instantiate. Hoist to module scope when the locale/options are static.
      
      **Incorrect (new formatter every render):**
      
      ```tsx
      function Price({ amount }: { amount: number }) {
        const formatter = new Intl.NumberFormat('en-US', {
          style: 'currency',
          currency: 'USD',
        })
        return <Text>{formatter.format(amount)}</Text>
      }
      ```
      
      **Correct (hoisted to module scope):**
      
      ```tsx
      const currencyFormatter = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'USD',
      })
      
      function Price({ amount }: { amount: number }) {
        return <Text>{currencyFormatter.format(amount)}</Text>
      }
      ```
      
      **For dynamic locales, memoize:**
      
      ```tsx
      const dateFormatter = useMemo(
        () => new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }),
        [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 relativeFormatter = new Intl.RelativeTimeFormat('en-US', {
        numeric: 'auto',
      })
      ```
      
      Creating `Intl` objects is significantly more expensive than `RegExp` or plain
      objects—each instantiation parses locale data and builds internal lookup tables.
      
    • list-performance-callbacks.md 1 KB
      ---
      title: Hoist callbacks to the root of lists
      impact: MEDIUM
      impactDescription: Fewer re-renders and faster lists
      tags: tag1, tag2
      ---
      
      ## List performance callbacks
      
      **Impact: HIGH (Fewer re-renders and faster lists)**
      
      When passing callback functions to list items, create a single instance of the
      callback at the root of the list. Items should then call it with a unique
      identifier.
      
      **Incorrect (creates a new callback on each render):**
      
      ```typescript
      return (
        <LegendList
          renderItem={({ item }) => {
            // bad: creates a new callback on each render
            const onPress = () => handlePress(item.id)
            return <Item key={item.id} item={item} onPress={onPress} />
          }}
        />
      )
      ```
      
      **Correct (a single function instance passed to each item):**
      
      ```typescript
      const onPress = useCallback(() => handlePress(item.id), [handlePress, item.id])
      
      return (
        <LegendList
          renderItem={({ item }) => (
            <Item key={item.id} item={item} onPress={onPress} />
          )}
        />
      )
      ```
      
      Reference: [Link to documentation or resource](https://example.com)
      
    • list-performance-function-references.md 4 KB
      ---
      title: Optimize List Performance with Stable Object References
      impact: CRITICAL
      impactDescription: virtualization relies on reference stability
      tags: lists, performance, flatlist, virtualization
      ---
      
      ## Optimize List Performance with Stable Object References
      
      Don't map or filter data before passing to virtualized lists. Virtualization
      relies on object reference stability to know what changed—new references cause
      full re-renders of all visible items. Attempt to prevent frequent renders at the
      list-parent level.
      
      Where needed, use context selectors within list items.
      
      **Incorrect (creates new object references on every keystroke):**
      
      ```tsx
      function DomainSearch() {
        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 (
          <>
            <TextInput value={keyword} onChangeText={setKeyword} />
            <LegendList
              data={domains}
              renderItem={({ item }) => <DomainItem item={item} keyword={keyword} />}
            />
          </>
        )
      }
      ```
      
      **Correct (stable references, transform inside items):**
      
      ```tsx
      const renderItem = ({ item }) => <DomainItem tld={item} />
      
      function DomainSearch() {
        const { data: tlds } = useTlds()
      
        return (
          <LegendList
            // good: as long as the data is stable, LegendList will not re-render the entire list
            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>
      }
      ```
      
      **Updating parent array reference:**
      
      Creating a new array instance can be okay, as long as its inner object
      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))
      
      return <LegendList data={sortedTlds} renderItem={renderItem} />
      ```
      
      Even though this creates a new array instance `sortedTlds`, the inner object
      references are stable.
      
      **With zustand for dynamic data (avoids parent re-renders):**
      
      ```tsx
      const useSearchStore = create<{ keyword: string }>(() => ({ keyword: '' }))
      
      function DomainSearch() {
        const { data: tlds } = useTlds()
      
        return (
          <>
            <SearchInput />
            <LegendList
              data={tlds}
              // if you aren't using React Compiler, wrap renderItem with useCallback
              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>
      }
      ```
      
      Virtualization can now skip items that haven't changed when typing. Only visible
      items (~20) re-render on keystroke, rather than the parent.
      
      **Deriving state within list items based on parent data (avoids parent
      re-renders):**
      
      For components where the data is conditional based on the parent state, this
      pattern is even more important. For example, if you are checking if an item is
      favorited, toggling favorites only re-renders one component if the item itself
      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} />
      }
      ```
      
      Note: if you're using the React Compiler, you can read React Context values
      directly within list items. Although this is slightly slower than using a
      Zustand selector in most cases, the effect may be negligible.
      
    • list-performance-images.md 1.4 KB
      ---
      title: Use Compressed Images in Lists
      impact: HIGH
      impactDescription: faster load times, less memory
      tags: lists, images, performance, optimization
      ---
      
      ## Use Compressed Images in Lists
      
      Always load compressed, appropriately-sized images in lists. Full-resolution
      images consume excessive memory and cause scroll jank. Request thumbnails from
      your server or use an image CDN with resize parameters.
      
      **Incorrect (full-resolution images):**
      
      ```tsx
      function ProductItem({ product }: { product: Product }) {
        return (
          <View>
            {/* 4000x3000 image loaded for a 100x100 thumbnail */}
            <Image
              source={{ uri: product.imageUrl }}
              style={{ width: 100, height: 100 }}
            />
            <Text>{product.name}</Text>
          </View>
        )
      }
      ```
      
      **Correct (request appropriately-sized image):**
      
      ```tsx
      function ProductItem({ product }: { product: Product }) {
        // Request a 200x200 image (2x for retina)
        const thumbnailUrl = `${product.imageUrl}?w=200&h=200&fit=cover`
      
        return (
          <View>
            <Image
              source={{ uri: thumbnailUrl }}
              style={{ width: 100, height: 100 }}
              contentFit='cover'
            />
            <Text>{product.name}</Text>
          </View>
        )
      }
      ```
      
      Use an optimized image component with built-in caching and placeholder support,
      such as `expo-image` or `SolitoImage` (which uses `expo-image` under the hood).
      Request images at 2x the display size for retina screens.
      
    • list-performance-inline-objects.md 2.3 KB
      ---
      title: Avoid Inline Objects in renderItem
      impact: HIGH
      impactDescription: prevents unnecessary re-renders of memoized list items
      tags: lists, performance, flatlist, virtualization, memo
      ---
      
      ## Avoid Inline Objects in renderItem
      
      Don't create new objects inside `renderItem` to pass as props. Inline objects
      create new references on every render, breaking memoization. Pass primitive
      values directly from `item` instead.
      
      **Incorrect (inline object breaks memoization):**
      
      ```tsx
      function UserList({ users }: { users: User[] }) {
        return (
          <LegendList
            data={users}
            renderItem={({ item }) => (
              <UserRow
                // Bad: new object on every render
                user={{ id: item.id, name: item.name, avatar: item.avatar }}
              />
            )}
          />
        )
      }
      ```
      
      **Incorrect (inline style object):**
      
      ```tsx
      renderItem={({ item }) => (
        <UserRow
          name={item.name}
          // Bad: new style object on every render
          style={{ backgroundColor: item.isActive ? 'green' : 'gray' }}
        />
      )}
      ```
      
      **Correct (pass item directly or primitives):**
      
      ```tsx
      function UserList({ users }: { users: User[] }) {
        return (
          <LegendList
            data={users}
            renderItem={({ item }) => (
              // Good: pass the item directly
              <UserRow user={item} />
            )}
          />
        )
      }
      ```
      
      **Correct (pass primitives, derive inside child):**
      
      ```tsx
      renderItem={({ item }) => (
        <UserRow
          id={item.id}
          name={item.name}
          isActive={item.isActive}
        />
      )}
      
      const UserRow = memo(function UserRow({ id, name, isActive }: Props) {
        // Good: derive style inside memoized component
        const backgroundColor = isActive ? 'green' : 'gray'
        return <View style={[styles.row, { backgroundColor }]}>{/* ... */}</View>
      })
      ```
      
      **Correct (hoist static styles in module scope):**
      
      ```tsx
      const activeStyle = { backgroundColor: 'green' }
      const inactiveStyle = { backgroundColor: 'gray' }
      
      renderItem={({ item }) => (
        <UserRow
          name={item.name}
          // Good: stable references
          style={item.isActive ? activeStyle : inactiveStyle}
        />
      )}
      ```
      
      Passing primitives or stable references allows `memo()` to skip re-renders when
      the actual values haven't changed.
      
      **Note:** If you have the React Compiler enabled, it handles memoization
      automatically and these manual optimizations become less critical.
      
    • list-performance-item-expensive.md 2.4 KB
      ---
      title: Keep List Items Lightweight
      impact: HIGH
      impactDescription: reduces render time for visible items during scroll
      tags: lists, performance, virtualization, hooks
      ---
      
      ## Keep List Items Lightweight
      
      List items should be as inexpensive as possible to render. Minimize hooks, avoid
      queries, and limit React Context access. Virtualized lists render many items
      during scroll—expensive items cause jank.
      
      **Incorrect (heavy list item):**
      
      ```tsx
      function ProductRow({ id }: { id: string }) {
        // Bad: query inside list item
        const { data: product } = useQuery(['product', id], () => fetchProduct(id))
        // Bad: multiple context accesses
        const theme = useContext(ThemeContext)
        const user = useContext(UserContext)
        const cart = useContext(CartContext)
        // Bad: expensive computation
        const recommendations = useMemo(
          () => computeRecommendations(product),
          [product]
        )
      
        return <View>{/* ... */}</View>
      }
      ```
      
      **Correct (lightweight list item):**
      
      ```tsx
      function ProductRow({ name, price, imageUrl }: Props) {
        // Good: receives only primitives, minimal hooks
        return (
          <View>
            <Image source={{ uri: imageUrl }} />
            <Text>{name}</Text>
            <Text>{price}</Text>
          </View>
        )
      }
      ```
      
      **Move data fetching to parent:**
      
      ```tsx
      // Parent fetches all data once
      function ProductList() {
        const { data: products } = useQuery(['products'], fetchProducts)
      
        return (
          <LegendList
            data={products}
            renderItem={({ item }) => (
              <ProductRow name={item.name} price={item.price} imageUrl={item.image} />
            )}
          />
        )
      }
      ```
      
      **For shared values, use Zustand selectors instead of Context:**
      
      ```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)
        // ...
      }
      
      // 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))
        // ...
      }
      ```
      
      **Guidelines for list items:**
      
      - No queries or data fetching
      - No expensive computations (move to parent or memoize at parent level)
      - Prefer Zustand selectors over React Context
      - Minimize useState/useEffect hooks
      - Pass pre-computed values as props
      
      The goal: list items should be simple rendering functions that take props and
      return JSX.
      
    • list-performance-item-memo.md 2.2 KB
      ---
      title: Pass Primitives to List Items for Memoization
      impact: HIGH
      impactDescription: enables effective memo() comparison
      tags: lists, performance, memo, primitives
      ---
      
      ## Pass Primitives to List Items for Memoization
      
      When possible, pass only primitive values (strings, numbers, booleans) as props
      to list item components. Primitives enable shallow comparison in `memo()` to
      work correctly, skipping re-renders when values haven't changed.
      
      **Incorrect (object prop requires deep comparison):**
      
      ```tsx
      type User = { id: string; name: string; email: string; avatar: string }
      
      const UserRow = memo(function UserRow({ user }: { user: User }) {
        // memo() compares user by reference, not value
        // If parent creates new user object, this re-renders even if data is same
        return <Text>{user.name}</Text>
      })
      
      renderItem={({ item }) => <UserRow user={item} />}
      ```
      
      This can still be optimized, but it is harder to memoize properly.
      
      **Correct (primitive props enable shallow comparison):**
      
      ```tsx
      const UserRow = memo(function UserRow({
        id,
        name,
        email,
      }: {
        id: string
        name: string
        email: string
      }) {
        // memo() compares each primitive directly
        // Re-renders only if id, name, or email actually changed
        return <Text>{name}</Text>
      })
      
      renderItem={({ item }) => (
        <UserRow id={item.id} name={item.name} email={item.email} />
      )}
      ```
      
      **Pass only what you need:**
      
      ```tsx
      // Incorrect: passing entire item when you only need name
      <UserRow user={item} />
      
      // Correct: pass only the fields the component uses
      <UserRow name={item.name} avatarUrl={item.avatar} />
      ```
      
      **For callbacks, hoist or use item ID:**
      
      ```tsx
      // Incorrect: inline function creates new reference
      <UserRow name={item.name} onPress={() => handlePress(item.id)} />
      
      // Correct: pass ID, handle in child
      <UserRow id={item.id} name={item.name} />
      
      const UserRow = memo(function UserRow({ id, name }: Props) {
        const handlePress = useCallback(() => {
          // use id here
        }, [id])
        return <Pressable onPress={handlePress}><Text>{name}</Text></Pressable>
      })
      ```
      
      Primitive props make memoization predictable and effective.
      
      **Note:** If you have the React Compiler enabled, you do not need to use
      `memo()` or `useCallback()`, but the object references still apply.
      
    • list-performance-item-types.md 2.7 KB
      ---
      title: Use Item Types for Heterogeneous Lists
      impact: HIGH
      impactDescription: efficient recycling, less layout thrashing
      tags: list, performance, recycling, heterogeneous, LegendList
      ---
      
      ## Use Item Types for Heterogeneous Lists
      
      When a list has different item layouts (messages, images, headers, etc.), use a
      `type` field on each item and provide `getItemType` to the list. This puts items
      into separate recycling pools so a message component never gets recycled into an
      image component.
      
      **Incorrect (single component with conditionals):**
      
      ```tsx
      type Item = { id: string; text?: string; imageUrl?: string; isHeader?: boolean }
      
      function ListItem({ item }: { item: Item }) {
        if (item.isHeader) {
          return <HeaderItem title={item.text} />
        }
        if (item.imageUrl) {
          return <ImageItem url={item.imageUrl} />
        }
        return <MessageItem text={item.text} />
      }
      
      function Feed({ items }: { items: Item[] }) {
        return (
          <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
      
      function Feed({ items }: { items: FeedItem[] }) {
        return (
          <LegendList
            data={items}
            keyExtractor={(item) => item.id}
            getItemType={(item) => item.type}
            renderItem={({ item }) => {
              switch (item.type) {
                case 'header':
                  return <SectionHeader title={item.title} />
                case 'message':
                  return <MessageRow text={item.text} />
                case 'image':
                  return <ImageRow url={item.url} />
              }
            }}
            recycleItems
          />
        )
      }
      ```
      
      **Why this matters:**
      
      - **Recycling efficiency**: Items with the same type share a recycling pool
      - **No layout thrashing**: A header never recycles into an image cell
      - **Type safety**: TypeScript can narrow the item type in each branch
      - **Better size estimation**: Use `getEstimatedItemSize` with `itemType` for
        accurate estimates per type
      
      ```tsx
      <LegendList
        data={items}
        keyExtractor={(item) => item.id}
        getItemType={(item) => item.type}
        getEstimatedItemSize={(index, item, itemType) => {
          switch (itemType) {
            case 'header':
              return 48
            case 'message':
              return 72
            case 'image':
              return 300
            default:
              return 72
          }
        }}
        renderItem={({ item }) => {
          /* ... */
        }}
        recycleItems
      />
      ```
      
      Reference:
      [LegendList getItemType](https://legendapp.com/open-source/list/api/props/#getitemtype-v2)
      
    • list-performance-virtualize.md 1.7 KB
      ---
      title: Use a List Virtualizer for Any List
      impact: HIGH
      impactDescription: reduced memory, faster mounts
      tags: lists, performance, virtualization, scrollview
      ---
      
      ## Use a List Virtualizer for Any List
      
      Use a list virtualizer like LegendList or FlashList instead of ScrollView with
      mapped children—even for short lists. Virtualizers only render visible items,
      reducing memory usage and mount time. ScrollView renders all children upfront,
      which gets expensive quickly.
      
      **Incorrect (ScrollView renders all items at once):**
      
      ```tsx
      function Feed({ items }: { items: Item[] }) {
        return (
          <ScrollView>
            {items.map((item) => (
              <ItemCard key={item.id} item={item} />
            ))}
          </ScrollView>
        )
      }
      // 50 items = 50 components mounted, even if only 10 visible
      ```
      
      **Correct (virtualizer renders only visible items):**
      
      ```tsx
      import { LegendList } from '@legendapp/list'
      
      function Feed({ items }: { items: Item[] }) {
        return (
          <LegendList
            data={items}
            // if you aren't using React Compiler, wrap these with useCallback
            renderItem={({ item }) => <ItemCard item={item} />}
            keyExtractor={(item) => item.id}
            estimatedItemSize={80}
          />
        )
      }
      // Only ~10-15 visible items mounted at a time
      ```
      
      **Alternative (FlashList):**
      
      ```tsx
      import { FlashList } from '@shopify/flash-list'
      
      function Feed({ items }: { items: Item[] }) {
        return (
          <FlashList
            data={items}
            // if you aren't using React Compiler, wrap these with useCallback
            renderItem={({ item }) => <ItemCard item={item} />}
            keyExtractor={(item) => item.id}
          />
        )
      }
      ```
      
      Benefits apply to any screen with scrollable content—profiles, settings, feeds,
      search results. Default to virtualization.
      
    • monorepo-native-deps-in-app.md 1.1 KB
      ---
      title: Install Native Dependencies in App Directory
      impact: CRITICAL
      impactDescription: required for autolinking to work
      tags: monorepo, native, autolinking, installation
      ---
      
      ## Install Native Dependencies in App Directory
      
      In a monorepo, packages with native code must be installed in the native app's
      directory directly. Autolinking only scans the app's `node_modules`—it won't
      find native dependencies installed in other packages.
      
      **Incorrect (native dep in shared package only):**
      
      ```
      packages/
        ui/
          package.json  # has react-native-reanimated
        app/
          package.json  # missing react-native-reanimated
      ```
      
      Autolinking fails—native code not linked.
      
      **Correct (native dep in app directory):**
      
      ```
      packages/
        ui/
          package.json  # has react-native-reanimated
        app/
          package.json  # also has react-native-reanimated
      ```
      
      ```json
      // packages/app/package.json
      {
        "dependencies": {
          "react-native-reanimated": "3.16.1"
        }
      }
      ```
      
      Even if the shared package uses the native dependency, the app must also list it
      for autolinking to detect and link the native code.
      
    • monorepo-single-dependency-versions.md 1.3 KB
      ---
      title: Use Single Dependency Versions Across Monorepo
      impact: MEDIUM
      impactDescription: avoids duplicate bundles, version conflicts
      tags: monorepo, dependencies, installation
      ---
      
      ## Use Single Dependency Versions Across Monorepo
      
      Use a single version of each dependency across all packages in your monorepo.
      Prefer exact versions over ranges. Multiple versions cause duplicate code in
      bundles, runtime conflicts, and inconsistent behavior across packages.
      
      Use a tool like syncpack to enforce this. As a last resort, use yarn resolutions
      or npm overrides.
      
      **Incorrect (version ranges, multiple versions):**
      
      ```json
      // packages/app/package.json
      {
        "dependencies": {
          "react-native-reanimated": "^3.0.0"
        }
      }
      
      // packages/ui/package.json
      {
        "dependencies": {
          "react-native-reanimated": "^3.5.0"
        }
      }
      ```
      
      **Correct (exact versions, single source of truth):**
      
      ```json
      // package.json (root)
      {
        "pnpm": {
          "overrides": {
            "react-native-reanimated": "3.16.1"
          }
        }
      }
      
      // packages/app/package.json
      {
        "dependencies": {
          "react-native-reanimated": "3.16.1"
        }
      }
      
      // packages/ui/package.json
      {
        "dependencies": {
          "react-native-reanimated": "3.16.1"
        }
      }
      ```
      
      Use your package manager's override/resolution feature to enforce versions at
      the root. When adding dependencies, specify exact versions without `^` or `~`.
      
    • navigation-native-navigators.md 4.8 KB
      ---
      title: Use Native Navigators for Navigation
      impact: HIGH
      impactDescription: native performance, platform-appropriate UI
      tags: navigation, react-navigation, expo-router, native-stack, tabs
      ---
      
      ## Use Native Navigators for Navigation
      
      Always use native navigators instead of JS-based ones. Native navigators use
      platform APIs (UINavigationController on iOS, Fragment on Android) for better
      performance and native behavior.
      
      **For stacks:** Use `@react-navigation/native-stack` or expo-router's default
      stack (which uses native-stack). Avoid `@react-navigation/stack`.
      
      **For tabs:** Use `react-native-bottom-tabs` (native) or expo-router's native
      tabs. Avoid `@react-navigation/bottom-tabs` when native feel matters.
      
      ### Stack Navigation
      
      **Incorrect (JS stack navigator):**
      
      ```tsx
      import { createStackNavigator } from '@react-navigation/stack'
      
      const Stack = createStackNavigator()
      
      function App() {
        return (
          <Stack.Navigator>
            <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'
      
      const Stack = createNativeStackNavigator()
      
      function App() {
        return (
          <Stack.Navigator>
            <Stack.Screen name='Home' component={HomeScreen} />
            <Stack.Screen name='Details' component={DetailsScreen} />
          </Stack.Navigator>
        )
      }
      ```
      
      **Correct (expo-router uses native stack by default):**
      
      ```tsx
      // app/_layout.tsx
      import { Stack } from 'expo-router'
      
      export default function Layout() {
        return <Stack />
      }
      ```
      
      ### Tab Navigation
      
      **Incorrect (JS bottom tabs):**
      
      ```tsx
      import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
      
      const Tab = createBottomTabNavigator()
      
      function App() {
        return (
          <Tab.Navigator>
            <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'
      
      const Tab = createNativeBottomTabNavigator()
      
      function App() {
        return (
          <Tab.Navigator>
            <Tab.Screen
              name='Home'
              component={HomeScreen}
              options={{
                tabBarIcon: () => ({ sfSymbol: 'house' }),
              }}
            />
            <Tab.Screen
              name='Settings'
              component={SettingsScreen}
              options={{
                tabBarIcon: () => ({ sfSymbol: 'gear' }),
              }}
            />
          </Tab.Navigator>
        )
      }
      ```
      
      **Correct (expo-router native tabs):**
      
      ```tsx
      // app/(tabs)/_layout.tsx
      import { NativeTabs } from 'expo-router/unstable-native-tabs'
      
      export default function TabLayout() {
        return (
          <NativeTabs>
            <NativeTabs.Trigger name='index'>
              <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
              <NativeTabs.Trigger.Icon sf='house.fill' md='home' />
            </NativeTabs.Trigger>
            <NativeTabs.Trigger name='settings'>
              <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
              <NativeTabs.Trigger.Icon sf='gear' md='settings' />
            </NativeTabs.Trigger>
          </NativeTabs>
        )
      }
      ```
      
      On iOS, native tabs automatically enable `contentInsetAdjustmentBehavior` on the
      first `ScrollView` at the root of each tab screen, so content scrolls correctly
      behind the translucent tab bar. If you need to disable this, use
      `disableAutomaticContentInsets` on the trigger.
      
      ### Prefer Native Header Options Over Custom Components
      
      **Incorrect (custom header component):**
      
      ```tsx
      <Stack.Screen
        name='Profile'
        component={ProfileScreen}
        options={{
          header: () => <CustomHeader title='Profile' />,
        }}
      />
      ```
      
      **Correct (native header options):**
      
      ```tsx
      <Stack.Screen
        name='Profile'
        component={ProfileScreen}
        options={{
          title: 'Profile',
          headerLargeTitleEnabled: true,
          headerSearchBarOptions: {
            placeholder: 'Search',
          },
        }}
      />
      ```
      
      Native headers support iOS large titles, search bars, blur effects, and proper
      safe area handling automatically.
      
      ### Why Native Navigators
      
      - **Performance**: Native transitions and gestures run on the UI thread
      - **Platform behavior**: Automatic iOS large titles, Android material design
      - **System integration**: Scroll-to-top on tab tap, PiP avoidance, proper safe
        areas
      - **Accessibility**: Platform accessibility features work automatically
      
      Reference:
      
      - [React Navigation Native Stack](https://reactnavigation.org/docs/native-stack-navigator)
      - [React Native Bottom Tabs with React Navigation](https://oss.callstack.com/react-native-bottom-tabs/docs/guides/usage-with-react-navigation)
      - [React Native Bottom Tabs with Expo Router](https://oss.callstack.com/react-native-bottom-tabs/docs/guides/usage-with-expo-router)
      - [Expo Router Native Tabs](https://docs.expo.dev/router/advanced/native-tabs)
      
    • react-compiler-destructure-functions.md 1.2 KB
      ---
      title: Destructure Functions Early in Render (React Compiler)
      impact: HIGH
      impactDescription: stable references, fewer re-renders
      tags: rerender, hooks, performance, react-compiler
      ---
      
      ## Destructure Functions Early in Render
      
      This rule is only applicable if you are using the React Compiler.
      
      Destructure functions from hooks at the top of render scope. Never dot into
      objects to call functions. Destructured functions are stable references; dotting
      creates new references and breaks memoization.
      
      **Incorrect (dotting into object):**
      
      ```tsx
      import { useRouter } from 'expo-router'
      
      function SaveButton(props) {
        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
        }
      
        return <Button onPress={handlePress}>Save</Button>
      }
      ```
      
      **Correct (destructure early):**
      
      ```tsx
      import { useRouter } from 'expo-router'
      
      function SaveButton({ onSave }) {
        const { push } = useRouter()
      
        // good: react-compiler will key on push and onSave
        const handlePress = () => {
          onSave()
          push('/success') // stable reference
        }
      
        return <Button onPress={handlePress}>Save</Button>
      }
      ```
      
    • react-compiler-reanimated-shared-values.md 1.2 KB
      ---
      title: Use .get() and .set() for Reanimated Shared Values (not .value)
      impact: LOW
      impactDescription: required for React Compiler compatibility
      tags: reanimated, react-compiler, shared-values
      ---
      
      ## Use .get() and .set() for Shared Values with React Compiler
      
      With React Compiler enabled, use `.get()` and `.set()` instead of reading or
      writing `.value` directly on Reanimated shared values. The compiler can't track
      property access—explicit methods ensure correct behavior.
      
      **Incorrect (breaks with React Compiler):**
      
      ```tsx
      import { useSharedValue } from 'react-native-reanimated'
      
      function Counter() {
        const count = useSharedValue(0)
      
        const increment = () => {
          count.value = count.value + 1 // opts out of react compiler
        }
      
        return <Button onPress={increment} title={`Count: ${count.value}`} />
      }
      ```
      
      **Correct (React Compiler compatible):**
      
      ```tsx
      import { useSharedValue } from 'react-native-reanimated'
      
      function Counter() {
        const count = useSharedValue(0)
      
        const increment = () => {
          count.set(count.get() + 1)
        }
      
        return <Button onPress={increment} title={`Count: ${count.get()}`} />
      }
      ```
      
      See the
      [Reanimated docs](https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support)
      for more.
      
    • react-state-dispatcher.md 2.2 KB
      ---
      title: useState Dispatch updaters for State That Depends on Current Value
      impact: MEDIUM
      impactDescription: avoids stale closures, prevents unnecessary re-renders
      tags: state, hooks, useState, callbacks
      ---
      
      ## Use Dispatch Updaters for State That Depends on Current Value
      
      When the next state depends on the current state, use a dispatch updater
      (`setState(prev => ...)`) instead of reading the state variable directly in a
      callback. This avoids stale closures and ensures you're comparing against the
      latest value.
      
      **Incorrect (reads state directly):**
      
      ```tsx
      const [size, setSize] = useState<Size | undefined>(undefined)
      
      const onLayout = (e: LayoutChangeEvent) => {
        const { width, height } = e.nativeEvent.layout
        // size may be stale in this closure
        if (size?.width !== width || size?.height !== height) {
          setSize({ width, height })
        }
      }
      ```
      
      **Correct (dispatch updater):**
      
      ```tsx
      const [size, setSize] = useState<Size | undefined>(undefined)
      
      const onLayout = (e: LayoutChangeEvent) => {
        const { width, height } = e.nativeEvent.layout
        setSize((prev) => {
          if (prev?.width === width && prev?.height === height) return prev
          return { width, height }
        })
      }
      ```
      
      Returning the previous value from the updater skips the re-render.
      
      For primitive states, you don't need to compare values before firing a
      re-render.
      
      **Incorrect (unnecessary comparison for primitive state):**
      
      ```tsx
      const [size, setSize] = useState<Size | undefined>(undefined)
      
      const onLayout = (e: LayoutChangeEvent) => {
        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 onLayout = (e: LayoutChangeEvent) => {
        const { width, height } = e.nativeEvent.layout
        setSize(width)
      }
      ```
      
      However, if the next state depends on the current state, you should still use a
      dispatch updater.
      
      **Incorrect (reads state directly from the callback):**
      
      ```tsx
      const [count, setCount] = useState(0)
      
      const onTap = () => {
        setCount(count + 1)
      }
      ```
      
      **Correct (dispatch updater):**
      
      ```tsx
      const [count, setCount] = useState(0)
      
      const onTap = () => {
        setCount((prev) => prev + 1)
      }
      ```
      
    • react-state-fallback.md 1.7 KB
      ---
      title: Use fallback state instead of initialState
      impact: MEDIUM
      impactDescription: reactive fallbacks without syncing
      tags: state, hooks, derived-state, props, initialState
      ---
      
      ## Use fallback state instead of initialState
      
      Use `undefined` as initial state and nullish coalescing (`??`) to fall back to
      parent or server values. State represents user intent only—`undefined` means
      "user hasn't chosen yet." This enables reactive fallbacks that update when the
      source changes, not just on initial render.
      
      **Incorrect (syncs state, loses reactivity):**
      
      ```tsx
      type Props = { fallbackEnabled: boolean }
      
      function Toggle({ fallbackEnabled }: Props) {
        const [enabled, setEnabled] = useState(defaultEnabled)
        // If fallbackEnabled changes, state is stale
        // State mixes user intent with default value
      
        return <Switch value={enabled} onValueChange={setEnabled} />
      }
      ```
      
      **Correct (state is user intent, reactive fallback):**
      
      ```tsx
      type Props = { fallbackEnabled: boolean }
      
      function Toggle({ fallbackEnabled }: Props) {
        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} />
      }
      ```
      
      **With server data:**
      
      ```tsx
      function ProfileForm({ data }: { data: User }) {
        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} />
      }
      ```
      
    • react-state-minimize.md 1.7 KB
      ---
      title: Minimize State Variables and Derive Values
      impact: MEDIUM
      impactDescription: fewer re-renders, less state drift
      tags: state, derived-state, hooks, optimization
      ---
      
      ## Minimize State Variables and Derive Values
      
      Use 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.
      
      **Incorrect (redundant state):**
      
      ```tsx
      function Cart({ items }: { items: Item[] }) {
        const [total, setTotal] = useState(0)
        const [itemCount, setItemCount] = useState(0)
      
        useEffect(() => {
          setTotal(items.reduce((sum, item) => sum + item.price, 0))
          setItemCount(items.length)
        }, [items])
      
        return (
          <View>
            <Text>{itemCount} items</Text>
            <Text>Total: ${total}</Text>
          </View>
        )
      }
      ```
      
      **Correct (derived values):**
      
      ```tsx
      function Cart({ items }: { items: Item[] }) {
        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>
        )
      }
      ```
      
      **Another example:**
      
      ```tsx
      // Incorrect: storing both firstName, lastName, AND fullName
      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}`
      ```
      
      State should be the minimal source of truth. Everything else is derived.
      
      Reference: [Choosing the State Structure](https://react.dev/learn/choosing-the-state-structure)
      
    • rendering-no-falsy-and.md 1.8 KB
      ---
      title: Never Use && with Potentially Falsy Values
      impact: CRITICAL
      impactDescription: prevents production crash
      tags: rendering, conditional, jsx, crash
      ---
      
      ## Never Use && with Potentially Falsy Values
      
      Never use `{value && <Component />}` when `value` could be an empty string or
      `0`. These are falsy but JSX-renderable—React Native will try to render them as
      text outside a `<Text>` component, causing a hard crash in production.
      
      **Incorrect (crashes if count is 0 or name is ""):**
      
      ```tsx
      function Profile({ name, count }: { name: string; count: number }) {
        return (
          <View>
            {name && <Text>{name}</Text>}
            {count && <Text>{count} items</Text>}
          </View>
        )
      }
      // If name="" or count=0, renders the falsy value → crash
      ```
      
      **Correct (ternary with null):**
      
      ```tsx
      function Profile({ name, count }: { name: string; count: number }) {
        return (
          <View>
            {name ? <Text>{name}</Text> : null}
            {count ? <Text>{count} items</Text> : null}
          </View>
        )
      }
      ```
      
      **Correct (explicit boolean coercion):**
      
      ```tsx
      function Profile({ name, count }: { name: string; count: number }) {
        return (
          <View>
            {!!name && <Text>{name}</Text>}
            {!!count && <Text>{count} items</Text>}
          </View>
        )
      }
      ```
      
      **Best (early return):**
      
      ```tsx
      function Profile({ name, count }: { name: string; count: number }) {
        if (!name) return null
      
        return (
          <View>
            <Text>{name}</Text>
            {count > 0 ? <Text>{count} items</Text> : null}
          </View>
        )
      }
      ```
      
      Early returns are clearest. When using conditionals inline, prefer ternary or
      explicit boolean checks.
      
      **Lint rule:** Enable `react/jsx-no-leaked-render` from
      [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-leaked-render.md)
      to catch this automatically.
      
    • rendering-text-in-text-component.md 697 B
      ---
      title: Wrap Strings in Text Components
      impact: CRITICAL
      impactDescription: prevents runtime crash
      tags: rendering, text, core
      ---
      
      ## Wrap Strings in Text Components
      
      Strings must be rendered inside `<Text>`. React Native crashes if a string is a
      direct child of `<View>`.
      
      **Incorrect (crashes):**
      
      ```tsx
      import { View } from 'react-native'
      
      function Greeting({ name }: { name: string }) {
        return <View>Hello, {name}!</View>
      }
      // Error: Text strings must be rendered within a <Text> component.
      ```
      
      **Correct:**
      
      ```tsx
      import { View, Text } from 'react-native'
      
      function Greeting({ name }: { name: string }) {
        return (
          <View>
            <Text>Hello, {name}!</Text>
          </View>
        )
      }
      ```
      
    • scroll-position-no-state.md 1.9 KB
      ---
      title: Never Track Scroll Position in useState
      impact: HIGH
      impactDescription: prevents render thrashing during scroll
      tags: scroll, performance, reanimated, useRef
      ---
      
      ## Never Track Scroll Position in useState
      
      Never store scroll position in `useState`. Scroll events fire rapidly—state
      updates cause render thrashing and dropped frames. Use a Reanimated shared value
      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'
      
      function Feed() {
        const [scrollY, setScrollY] = useState(0)
      
        const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
          setScrollY(e.nativeEvent.contentOffset.y) // re-renders on every frame
        }
      
        return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />
      }
      ```
      
      **Correct (Reanimated for animations):**
      
      ```tsx
      import Animated, {
        useSharedValue,
        useAnimatedScrollHandler,
      } from 'react-native-reanimated'
      
      function Feed() {
        const scrollY = useSharedValue(0)
      
        const onScroll = useAnimatedScrollHandler({
          onScroll: (e) => {
            scrollY.value = e.contentOffset.y // runs on UI thread, no re-render
          },
        })
      
        return (
          <Animated.ScrollView
            onScroll={onScroll}
            // higher number has better performance, but it fires less often.
            // 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'
      
      function Feed() {
        const scrollY = useRef(0)
      
        const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
          scrollY.current = e.nativeEvent.contentOffset.y // no re-render
        }
      
        return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />
      }
      ```
      
    • state-ground-truth.md 2.1 KB
      ---
      title: State Must Represent Ground Truth
      impact: HIGH
      impactDescription: cleaner logic, easier debugging, single source of truth
      tags: state, derived-state, reanimated, hooks
      ---
      
      ## State Must Represent Ground Truth
      
      State variables—both React `useState` and Reanimated shared values—should
      represent the actual state of something (e.g., `pressed`, `progress`, `isOpen`),
      not derived visual values (e.g., `scale`, `opacity`, `translateY`). Derive
      visual values from state using computation or interpolation.
      
      **Incorrect (storing the visual output):**
      
      ```tsx
      const scale = useSharedValue(1)
      
      const tap = Gesture.Tap()
        .onBegin(() => {
          scale.set(withTiming(0.95))
        })
        .onFinalize(() => {
          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 tap = Gesture.Tap()
        .onBegin(() => {
          pressed.set(withTiming(1))
        })
        .onFinalize(() => {
          pressed.set(withTiming(0))
        })
      
      const animatedStyle = useAnimatedStyle(() => ({
        transform: [{ scale: interpolate(pressed.get(), [0, 1], [1, 0.95]) }],
      }))
      ```
      
      **Why this matters:**
      
      State variables should represent real "state", not necessarily a desired end
      result.
      
      1. **Single source of truth** — The state (`pressed`) describes what's
         happening; visuals are derived
      2. **Easier to extend** — Adding opacity, rotation, or other effects just
         requires more interpolations from the same state
      3. **Debugging** — Inspecting `pressed = 1` is clearer than `scale = 0.95`
      4. **Reusable logic** — The same `pressed` value can drive multiple visual
         properties
      
      **Same principle for React state:**
      
      ```tsx
      // Incorrect: storing derived values
      const [isExpanded, setIsExpanded] = useState(false)
      const [height, setHeight] = useState(0)
      
      useEffect(() => {
        setHeight(isExpanded ? 200 : 0)
      }, [isExpanded])
      
      // Correct: derive from state
      const [isExpanded, setIsExpanded] = useState(false)
      const height = isExpanded ? 200 : 0
      ```
      
      State is the minimal truth. Everything else is derived.
      
    • ui-expo-image.md 1.6 KB
      ---
      title: Use expo-image for Optimized Images
      impact: HIGH
      impactDescription: memory efficiency, caching, blurhash placeholders, progressive loading
      tags: images, performance, expo-image, ui
      ---
      
      ## Use expo-image for Optimized Images
      
      Use `expo-image` instead of React Native's `Image`. It provides memory-efficient caching, blurhash placeholders, progressive loading, and better performance for lists.
      
      **Incorrect (React Native Image):**
      
      ```tsx
      import { Image } from 'react-native'
      
      function Avatar({ url }: { url: string }) {
        return <Image source={{ uri: url }} style={styles.avatar} />
      }
      ```
      
      **Correct (expo-image):**
      
      ```tsx
      import { Image } from 'expo-image'
      
      function Avatar({ url }: { url: string }) {
        return <Image source={{ uri: url }} style={styles.avatar} />
      }
      ```
      
      **With blurhash placeholder:**
      
      ```tsx
      <Image
        source={{ uri: url }}
        placeholder={{ blurhash: 'LGF5]+Yk^6#M@-5c,1J5@[or[Q6.' }}
        contentFit="cover"
        transition={200}
        style={styles.image}
      />
      ```
      
      **With priority and caching:**
      
      ```tsx
      <Image
        source={{ uri: url }}
        priority="high"
        cachePolicy="memory-disk"
        style={styles.hero}
      />
      ```
      
      **Key props:**
      
      - `placeholder` — Blurhash or thumbnail while loading
      - `contentFit` — `cover`, `contain`, `fill`, `scale-down`
      - `transition` — Fade-in duration (ms)
      - `priority` — `low`, `normal`, `high`
      - `cachePolicy` — `memory`, `disk`, `memory-disk`, `none`
      - `recyclingKey` — Unique key for list recycling
      
      For cross-platform (web + native), use `SolitoImage` from `solito/image` which uses `expo-image` under the hood.
      
      Reference: [expo-image](https://docs.expo.dev/versions/latest/sdk/image/)
      
    • ui-image-gallery.md 2.5 KB
      ---
      title: Use Galeria for Image Galleries and Lightbox
      impact: MEDIUM
      impactDescription:
        native shared element transitions, pinch-to-zoom, pan-to-close
      tags: images, gallery, lightbox, expo-image, ui
      ---
      
      ## Use Galeria for Image Galleries and Lightbox
      
      For image galleries with lightbox (tap to fullscreen), use `@nandorojo/galeria`.
      It provides native shared element transitions with pinch-to-zoom, double-tap
      zoom, and pan-to-close. Works with any image component including `expo-image`.
      
      **Incorrect (custom modal implementation):**
      
      ```tsx
      function ImageGallery({ urls }: { urls: string[] }) {
        const [selected, setSelected] = useState<string | null>(null)
      
        return (
          <>
            {urls.map((url) => (
              <Pressable key={url} onPress={() => setSelected(url)}>
                <Image source={{ uri: url }} style={styles.thumbnail} />
              </Pressable>
            ))}
            <Modal visible={!!selected} onRequestClose={() => setSelected(null)}>
              <Image source={{ uri: selected! }} style={styles.fullscreen} />
            </Modal>
          </>
        )
      }
      ```
      
      **Correct (Galeria with expo-image):**
      
      ```tsx
      import { Galeria } from '@nandorojo/galeria'
      import { Image } from 'expo-image'
      
      function ImageGallery({ urls }: { urls: string[] }) {
        return (
          <Galeria urls={urls}>
            {urls.map((url, index) => (
              <Galeria.Image index={index} key={url}>
                <Image source={{ uri: url }} style={styles.thumbnail} />
              </Galeria.Image>
            ))}
          </Galeria>
        )
      }
      ```
      
      **Single image:**
      
      ```tsx
      import { Galeria } from '@nandorojo/galeria'
      import { Image } from 'expo-image'
      
      function Avatar({ url }: { url: string }) {
        return (
          <Galeria urls={[url]}>
            <Galeria.Image>
              <Image source={{ uri: url }} style={styles.avatar} />
            </Galeria.Image>
          </Galeria>
        )
      }
      ```
      
      **With low-res thumbnails and high-res fullscreen:**
      
      ```tsx
      <Galeria urls={highResUrls}>
        {lowResUrls.map((url, index) => (
          <Galeria.Image index={index} key={url}>
            <Image source={{ uri: url }} style={styles.thumbnail} />
          </Galeria.Image>
        ))}
      </Galeria>
      ```
      
      **With FlashList:**
      
      ```tsx
      <Galeria urls={urls}>
        <FlashList
          data={urls}
          renderItem={({ item, index }) => (
            <Galeria.Image index={index}>
              <Image source={{ uri: item }} style={styles.thumbnail} />
            </Galeria.Image>
          )}
          numColumns={3}
          estimatedItemSize={100}
        />
      </Galeria>
      ```
      
      Works with `expo-image`, `SolitoImage`, `react-native` Image, or any image
      component.
      
      Reference: [Galeria](https://github.com/nandorojo/galeria)
      
    • ui-measure-views.md 2.2 KB
      ---
      title: Measuring View Dimensions
      impact: MEDIUM
      impactDescription: synchronous measurement, avoid unnecessary re-renders
      tags: layout, measurement, onLayout, useLayoutEffect
      ---
      
      ## Measuring View Dimensions
      
      Use both `useLayoutEffect` (synchronous) and `onLayout` (for updates). The sync
      measurement gives you the initial size immediately; `onLayout` keeps it current
      when the view changes. For non-primitive states, use a dispatch updater to
      compare values and avoid unnecessary re-renders.
      
      **Height only:**
      
      ```tsx
      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)
      
        useLayoutEffect(() => {
          // Sync measurement on mount (RN 0.82+)
          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)
        }
      
        return (
          <View ref={ref} onLayout={onLayout}>
            {children}
          </View>
        )
      }
      ```
      
      **Both dimensions:**
      
      ```tsx
      import { useLayoutEffect, useRef, useState } from 'react'
      import { View, LayoutChangeEvent } from 'react-native'
      
      type Size = { width: number; height: number }
      
      function MeasuredBox({ children }: { children: React.ReactNode }) {
        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 onLayout = (e: LayoutChangeEvent) => {
          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 }
          })
        }
      
        return (
          <View ref={ref} onLayout={onLayout}>
            {children}
          </View>
        )
      }
      ```
      
      Use functional setState to compare—don't read state directly in the callback.
      
    • ui-menus.md 4.5 KB
      ---
      title: Use Native Menus for Dropdowns and Context Menus
      impact: HIGH
      impactDescription: native accessibility, platform-consistent UX
      tags: user-interface, menus, context-menus, zeego, accessibility
      ---
      
      ## Use Native Menus for Dropdowns and Context Menus
      
      Use native platform menus instead of custom JS implementations. Native menus
      provide built-in accessibility, consistent platform UX, and better performance.
      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'
      
      function MyMenu() {
        const [open, setOpen] = useState(false)
      
        return (
          <View>
            <Pressable onPress={() => setOpen(!open)}>
              <Text>Open Menu</Text>
            </Pressable>
            {open && (
              <View style={{ position: 'absolute', top: 40 }}>
                <Pressable onPress={() => console.log('edit')}>
                  <Text>Edit</Text>
                </Pressable>
                <Pressable onPress={() => console.log('delete')}>
                  <Text>Delete</Text>
                </Pressable>
              </View>
            )}
          </View>
        )
      }
      ```
      
      **Correct (native menu with zeego):**
      
      ```tsx
      import * as DropdownMenu from 'zeego/dropdown-menu'
      
      function MyMenu() {
        return (
          <DropdownMenu.Root>
            <DropdownMenu.Trigger>
              <Pressable>
                <Text>Open Menu</Text>
              </Pressable>
            </DropdownMenu.Trigger>
      
            <DropdownMenu.Content>
              <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.ItemTitle>Delete</DropdownMenu.ItemTitle>
              </DropdownMenu.Item>
            </DropdownMenu.Content>
          </DropdownMenu.Root>
        )
      }
      ```
      
      **Context menu (long-press):**
      
      ```tsx
      import * as ContextMenu from 'zeego/context-menu'
      
      function MyContextMenu() {
        return (
          <ContextMenu.Root>
            <ContextMenu.Trigger>
              <View style={{ padding: 20 }}>
                <Text>Long press me</Text>
              </View>
            </ContextMenu.Trigger>
      
            <ContextMenu.Content>
              <ContextMenu.Item key='copy' onSelect={() => console.log('copy')}>
                <ContextMenu.ItemTitle>Copy</ContextMenu.ItemTitle>
              </ContextMenu.Item>
      
              <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'
      
      function SettingsMenu() {
        const [notifications, setNotifications] = useState(true)
      
        return (
          <DropdownMenu.Root>
            <DropdownMenu.Trigger>
              <Pressable>
                <Text>Settings</Text>
              </Pressable>
            </DropdownMenu.Trigger>
      
            <DropdownMenu.Content>
              <DropdownMenu.CheckboxItem
                key='notifications'
                value={notifications}
                onValueChange={() => setNotifications((prev) => !prev)}
              >
                <DropdownMenu.ItemIndicator />
                <DropdownMenu.ItemTitle>Notifications</DropdownMenu.ItemTitle>
              </DropdownMenu.CheckboxItem>
            </DropdownMenu.Content>
          </DropdownMenu.Root>
        )
      }
      ```
      
      **Submenus:**
      
      ```tsx
      import * as DropdownMenu from 'zeego/dropdown-menu'
      
      function MenuWithSubmenu() {
        return (
          <DropdownMenu.Root>
            <DropdownMenu.Trigger>
              <Pressable>
                <Text>Options</Text>
              </Pressable>
            </DropdownMenu.Trigger>
      
            <DropdownMenu.Content>
              <DropdownMenu.Item key='home' onSelect={() => console.log('home')}>
                <DropdownMenu.ItemTitle>Home</DropdownMenu.ItemTitle>
              </DropdownMenu.Item>
      
              <DropdownMenu.Sub>
                <DropdownMenu.SubTrigger key='more'>
                  <DropdownMenu.ItemTitle>More Options</DropdownMenu.ItemTitle>
                </DropdownMenu.SubTrigger>
      
                <DropdownMenu.SubContent>
                  <DropdownMenu.Item key='settings'>
                    <DropdownMenu.ItemTitle>Settings</DropdownMenu.ItemTitle>
                  </DropdownMenu.Item>
      
                  <DropdownMenu.Item key='help'>
                    <DropdownMenu.ItemTitle>Help</DropdownMenu.ItemTitle>
                  </DropdownMenu.Item>
                </DropdownMenu.SubContent>
              </DropdownMenu.Sub>
            </DropdownMenu.Content>
          </DropdownMenu.Root>
        )
      }
      ```
      
      Reference: [Zeego Documentation](https://zeego.dev/components/dropdown-menu)
      
    • ui-native-modals.md 1.8 KB
      ---
      title: Use Native Modals Over JS-Based Bottom Sheets
      impact: HIGH
      impactDescription: native performance, gestures, accessibility
      tags: modals, bottom-sheet, native, react-navigation
      ---
      
      ## Use Native Modals Over JS-Based Bottom Sheets
      
      Use native `<Modal>` with `presentationStyle="formSheet"` or React Navigation
      v7's native form sheet instead of JS-based bottom sheet libraries. Native modals
      have built-in gestures, accessibility, and better performance. Rely on native UI
      for low-level primitives.
      
      **Incorrect (JS-based bottom sheet):**
      
      ```tsx
      import BottomSheet from 'custom-js-bottom-sheet'
      
      function MyScreen() {
        const sheetRef = useRef<BottomSheet>(null)
      
        return (
          <View style={{ flex: 1 }}>
            <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'
      
      function MyScreen() {
        const [visible, setVisible] = useState(false)
      
        return (
          <View style={{ flex: 1 }}>
            <Button onPress={() => setVisible(true)} title='Open' />
            <Modal
              visible={visible}
              presentationStyle='formSheet'
              animationType='slide'
              onRequestClose={() => setVisible(false)}
            >
              <View>
                <Text>Sheet content</Text>
              </View>
            </Modal>
          </View>
        )
      }
      ```
      
      **Correct (React Navigation v7 native form sheet):**
      
      ```tsx
      // In your navigator
      <Stack.Screen
        name='Details'
        component={DetailsScreen}
        options={{
          presentation: 'formSheet',
          sheetAllowedDetents: 'fitToContents',
        }}
      />
      ```
      
      Native modals provide swipe-to-dismiss, proper keyboard avoidance, and
      accessibility out of the box.
      
    • ui-pressable.md 1.5 KB
      ---
      title: Use Pressable Instead of Touchable Components
      impact: LOW
      impactDescription: modern API, more flexible
      tags: ui, pressable, touchable, gestures
      ---
      
      ## Use Pressable Instead of Touchable Components
      
      Never use `TouchableOpacity` or `TouchableHighlight`. Use `Pressable` from
      `react-native` or `react-native-gesture-handler` instead.
      
      **Incorrect (legacy Touchable components):**
      
      ```tsx
      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'
      
      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'
      
      function ListItem({ onPress }: { onPress: () => void }) {
        return (
          <Pressable onPress={onPress}>
            <Text>Item</Text>
          </Pressable>
        )
      }
      ```
      
      Use `react-native-gesture-handler` Pressable inside scrollable lists for better
      gesture coordination, as long as you are using the ScrollView from
      `react-native-gesture-handler` as well.
      
      **For animated press states (scale, opacity changes):** Use `GestureDetector`
      with Reanimated shared values instead of Pressable's style callback. See the
      `animation-gesture-detector-press` rule.
      
    • ui-safe-area-scroll.md 1.5 KB
      ---
      title: Use contentInsetAdjustmentBehavior for Safe Areas
      impact: MEDIUM
      impactDescription: native safe area handling, no layout shifts
      tags: safe-area, scrollview, layout
      ---
      
      ## Use contentInsetAdjustmentBehavior for Safe Areas
      
      Use `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.
      
      **Incorrect (SafeAreaView wrapper):**
      
      ```tsx
      import { SafeAreaView, ScrollView, View, Text } from 'react-native'
      
      function MyScreen() {
        return (
          <SafeAreaView style={{ flex: 1 }}>
            <ScrollView>
              <View>
                <Text>Content</Text>
              </View>
            </ScrollView>
          </SafeAreaView>
        )
      }
      ```
      
      **Incorrect (manual safe area padding):**
      
      ```tsx
      import { ScrollView, View, Text } from 'react-native'
      import { useSafeAreaInsets } from 'react-native-safe-area-context'
      
      function MyScreen() {
        const insets = useSafeAreaInsets()
      
        return (
          <ScrollView contentContainerStyle={{ paddingTop: insets.top }}>
            <View>
              <Text>Content</Text>
            </View>
          </ScrollView>
        )
      }
      ```
      
      **Correct (native content inset adjustment):**
      
      ```tsx
      import { ScrollView, View, Text } from 'react-native'
      
      function MyScreen() {
        return (
          <ScrollView contentInsetAdjustmentBehavior='automatic'>
            <View>
              <Text>Content</Text>
            </View>
          </ScrollView>
        )
      }
      ```
      
      The native approach handles dynamic safe areas (keyboard, toolbars) and allows content to scroll behind the status bar naturally.
      
    • ui-scrollview-content-inset.md 1.3 KB
      ---
      title: Use contentInset for Dynamic ScrollView Spacing
      impact: LOW
      impactDescription: smoother updates, no layout recalculation
      tags: scrollview, layout, contentInset, performance
      ---
      
      ## Use contentInset for Dynamic ScrollView Spacing
      
      When adding space to the top or bottom of a ScrollView that may change
      (keyboard, toolbars, dynamic content), use `contentInset` instead of padding.
      Changing `contentInset` doesn't trigger layout recalculation—it adjusts the
      scroll area without re-rendering content.
      
      **Incorrect (padding causes layout recalculation):**
      
      ```tsx
      function Feed({ bottomOffset }: { bottomOffset: number }) {
        return (
          <ScrollView contentContainerStyle={{ paddingBottom: bottomOffset }}>
            {children}
          </ScrollView>
        )
      }
      // Changing bottomOffset triggers full layout recalculation
      ```
      
      **Correct (contentInset for dynamic spacing):**
      
      ```tsx
      function Feed({ bottomOffset }: { bottomOffset: number }) {
        return (
          <ScrollView
            contentInset={{ bottom: bottomOffset }}
            scrollIndicatorInsets={{ bottom: bottomOffset }}
          >
            {children}
          </ScrollView>
        )
      }
      // Changing bottomOffset only adjusts scroll bounds
      ```
      
      Use `scrollIndicatorInsets` alongside `contentInset` to keep the scroll
      indicator aligned. For static spacing that never changes, padding is fine.
      
    • ui-styling.md 2.2 KB
      ---
      title: Modern React Native Styling Patterns
      impact: MEDIUM
      impactDescription: consistent design, smoother borders, cleaner layouts
      tags: styling, css, layout, shadows, gradients
      ---
      
      ## Modern React Native Styling Patterns
      
      Follow these styling patterns for cleaner, more consistent React Native code.
      
      **Always use `borderCurve: 'continuous'` with `borderRadius`:**
      
      ```tsx
      // Incorrect
      { borderRadius: 12 }
      
      // Correct – smoother iOS-style corners
      { borderRadius: 12, borderCurve: 'continuous' }
      ```
      
      **Use `gap` instead of margin for spacing between elements:**
      
      ```tsx
      // Incorrect – margin on children
      <View>
        <Text style={{ marginBottom: 8 }}>Title</Text>
        <Text style={{ marginBottom: 8 }}>Subtitle</Text>
      </View>
      
      // Correct – gap on parent
      <View style={{ gap: 8 }}>
        <Text>Title</Text>
        <Text>Subtitle</Text>
      </View>
      ```
      
      **Use `padding` for space within, `gap` for space between:**
      
      ```tsx
      <View style={{ padding: 16, gap: 12 }}>
        <Text>First</Text>
        <Text>Second</Text>
      </View>
      ```
      
      **Use `experimental_backgroundImage` for linear gradients:**
      
      ```tsx
      // Incorrect – third-party gradient library
      <LinearGradient colors={['#000', '#fff']} />
      
      // Correct – native CSS gradient syntax
      <View
        style={{
          experimental_backgroundImage: 'linear-gradient(to bottom, #000, #fff)',
        }}
      />
      ```
      
      **Use CSS `boxShadow` string syntax for shadows:**
      
      ```tsx
      // Incorrect – legacy shadow objects or elevation
      { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1 }
      { elevation: 4 }
      
      // Correct – CSS box-shadow syntax
      { boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' }
      ```
      
      **Avoid multiple font sizes – use weight and color for emphasis:**
      
      ```tsx
      // Incorrect – varying font sizes for hierarchy
      <Text style={{ fontSize: 18 }}>Title</Text>
      <Text style={{ fontSize: 14 }}>Subtitle</Text>
      <Text style={{ fontSize: 12 }}>Caption</Text>
      
      // Correct – consistent size, vary weight and color
      <Text style={{ fontWeight: '600' }}>Title</Text>
      <Text style={{ color: '#666' }}>Subtitle</Text>
      <Text style={{ color: '#999' }}>Caption</Text>
      ```
      
      Limiting font sizes creates visual consistency. Use `fontWeight` (bold/semibold)
      and grayscale colors for hierarchy instead.
      
    • _sections.md 2.2 KB
      # Sections
      
      This file defines all sections, their ordering, impact levels, and descriptions.
      The section ID (in parentheses) is the filename prefix used to group rules.
      
      ---
      
      ## 1. Core Rendering (rendering)
      
      **Impact:** CRITICAL  
      **Description:** Fundamental React Native rendering rules. Violations cause
      runtime crashes or broken UI.
      
      ## 2. List Performance (list-performance)
      
      **Impact:** HIGH  
      **Description:** Optimizing virtualized lists (FlatList, LegendList, FlashList)
      for smooth scrolling and fast updates.
      
      ## 3. Animation (animation)
      
      **Impact:** HIGH  
      **Description:** GPU-accelerated animations, Reanimated patterns, and avoiding
      render thrashing during gestures.
      
      ## 4. Scroll Performance (scroll)
      
      **Impact:** HIGH  
      **Description:** Tracking scroll position without causing render thrashing.
      
      ## 5. Navigation (navigation)
      
      **Impact:** HIGH  
      **Description:** Using native navigators for stack and tab navigation instead of
      JS-based alternatives.
      
      ## 6. React State (react-state)
      
      **Impact:** MEDIUM  
      **Description:** Patterns for managing React state to avoid stale closures and
      unnecessary re-renders.
      
      ## 7. State Architecture (state)
      
      **Impact:** MEDIUM  
      **Description:** Ground truth principles for state variables and derived values.
      
      ## 8. React Compiler (react-compiler)
      
      **Impact:** MEDIUM  
      **Description:** Compatibility patterns for React Compiler with React Native and
      Reanimated.
      
      ## 9. User Interface (ui)
      
      **Impact:** MEDIUM  
      **Description:** Native UI patterns for images, menus, modals, styling, and
      platform-consistent interfaces.
      
      ## 10. Design System (design-system)
      
      **Impact:** MEDIUM  
      **Description:** Architecture patterns for building maintainable component
      libraries.
      
      ## 11. Monorepo (monorepo)
      
      **Impact:** LOW  
      **Description:** Dependency management and native module configuration in
      monorepos.
      
      ## 12. Third-Party Dependencies (imports)
      
      **Impact:** LOW  
      **Description:** Wrapping and re-exporting third-party dependencies for
      maintainability.
      
      ## 13. JavaScript (js)
      
      **Impact:** LOW  
      **Description:** Micro-optimizations like hoisting expensive object creation.
      
      ## 14. Fonts (fonts)
      
      **Impact:** LOW  
      **Description:** Native font loading for improved performance.
      
    • _template.md 631 B
      ---
      title: Rule Title Here
      impact: MEDIUM
      impactDescription: Optional description of impact (e.g., "20-50% improvement")
      tags: tag1, tag2
      ---
      
      ## Rule Title Here
      
      **Impact: MEDIUM (optional impact description)**
      
      Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.
      
      **Incorrect (description of what's wrong):**
      
      ```typescript
      // Bad code example here
      const bad = example()
      ```
      
      **Correct (description of what's right):**
      
      ```typescript
      // Good code example here
      const good = example()
      ```
      
      Reference: [Link to documentation or resource](https://example.com)
      
  • SKILL.md 4.3 KB
    ---
    name: vercel-react-native-skills
    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.
    license: MIT
    metadata:
      author: vercel
      version: '1.0.0'
    ---
    
    # React Native Skills
    
    Comprehensive best practices for React Native and Expo applications. Contains
    rules across multiple categories covering performance, animations, UI patterns,
    and platform-specific optimizations.
    
    ## When to Apply
    
    Reference these guidelines when:
    
    - Building React Native or Expo apps
    - Optimizing list and scroll performance
    - Implementing animations with Reanimated
    - Working with images and media
    - Configuring native modules or fonts
    - Structuring monorepo projects with native dependencies
    
    ## Rule Categories by Priority
    
    | Priority | Category         | Impact   | Prefix               |
    | -------- | ---------------- | -------- | -------------------- |
    | 1        | List Performance | CRITICAL | `list-performance-`  |
    | 2        | Animation        | HIGH     | `animation-`         |
    | 3        | Navigation       | HIGH     | `navigation-`        |
    | 4        | UI Patterns      | HIGH     | `ui-`                |
    | 5        | State Management | MEDIUM   | `react-state-`       |
    | 6        | Rendering        | MEDIUM   | `rendering-`         |
    | 7        | Monorepo         | MEDIUM   | `monorepo-`          |
    | 8        | Configuration    | LOW      | `fonts-`, `imports-` |
    
    ## Quick Reference
    
    ### 1. List Performance (CRITICAL)
    
    - `list-performance-virtualize` - Use FlashList for large lists
    - `list-performance-item-memo` - Memoize list item components
    - `list-performance-callbacks` - Stabilize callback references
    - `list-performance-inline-objects` - Avoid inline style objects
    - `list-performance-function-references` - Extract functions outside render
    - `list-performance-images` - Optimize images in lists
    - `list-performance-item-expensive` - Move expensive work outside items
    - `list-performance-item-types` - Use item types for heterogeneous lists
    
    ### 2. Animation (HIGH)
    
    - `animation-gpu-properties` - Animate only transform and opacity
    - `animation-derived-value` - Use useDerivedValue for computed animations
    - `animation-gesture-detector-press` - Use Gesture.Tap instead of Pressable
    
    ### 3. Navigation (HIGH)
    
    - `navigation-native-navigators` - Use native stack and native tabs over JS navigators
    
    ### 4. UI Patterns (HIGH)
    
    - `ui-expo-image` - Use expo-image for all images
    - `ui-image-gallery` - Use Galeria for image lightboxes
    - `ui-pressable` - Use Pressable over TouchableOpacity
    - `ui-safe-area-scroll` - Handle safe areas in ScrollViews
    - `ui-scrollview-content-inset` - Use contentInset for headers
    - `ui-menus` - Use native context menus
    - `ui-native-modals` - Use native modals when possible
    - `ui-measure-views` - Use onLayout, not measure()
    - `ui-styling` - Use StyleSheet.create or Nativewind
    
    ### 5. State Management (MEDIUM)
    
    - `react-state-minimize` - Minimize state subscriptions
    - `react-state-dispatcher` - Use dispatcher pattern for callbacks
    - `react-state-fallback` - Show fallback on first render
    - `react-compiler-destructure-functions` - Destructure for React Compiler
    - `react-compiler-reanimated-shared-values` - Handle shared values with compiler
    
    ### 6. Rendering (MEDIUM)
    
    - `rendering-text-in-text-component` - Wrap text in Text components
    - `rendering-no-falsy-and` - Avoid falsy && for conditional rendering
    
    ### 7. Monorepo (MEDIUM)
    
    - `monorepo-native-deps-in-app` - Keep native dependencies in app package
    - `monorepo-single-dependency-versions` - Use single versions across packages
    
    ### 8. Configuration (LOW)
    
    - `fonts-config-plugin` - Use config plugins for custom fonts
    - `imports-design-system-folder` - Organize design system imports
    - `js-hoist-intl` - Hoist Intl object creation
    
    ## How to Use
    
    Read individual rule files for detailed explanations and code examples:
    
    ```
    rules/list-performance-virtualize.md
    rules/animation-gpu-properties.md
    ```
    
    Each rule file contains:
    
    - Brief explanation of why it matters
    - Incorrect code example with explanation
    - Correct code example with explanation
    - Additional context and references
    
    ## Full Compiled Document
    
    For the complete guide with all rules expanded: `AGENTS.md`
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related