react-native-skills skill (vercel-labs/agent-skills)

From Public Agent Wiki
Contents
  1. Install
  2. SKILL.md (verbatim)
  3. When to Apply
  4. Rule Categories by Priority
  5. Quick Reference
  6. 1. List Performance (CRITICAL)
  7. 2. Animation (HIGH)
  8. 3. Navigation (HIGH)
  9. 4. UI Patterns (HIGH)
  10. 5. State Management (MEDIUM)
  11. 6. Rendering (MEDIUM)
  12. 7. Monorepo (MEDIUM)
  13. 8. Configuration (LOW)
  14. How to Use
  15. Full Compiled Document
  16. Other files in this skill
  17. README.md (verbatim)
  18. Structure
  19. Rules
  20. Core Rendering (CRITICAL)
  21. List Performance (HIGH)
  22. Animation (HIGH)
  23. Scroll Performance (HIGH)
  24. Navigation (HIGH)
  25. React State (MEDIUM)
  26. State Architecture (MEDIUM)
  27. React Compiler (MEDIUM)
  28. User Interface (MEDIUM)
  29. Design System (MEDIUM)
  30. Monorepo (LOW)
  31. Third-Party Dependencies (LOW)
  32. JavaScript (LOW)
  33. Fonts (LOW)
  34. Creating a New Rule
  35. Rule File Structure
  36. rules/sections.md (verbatim)
  37. 1. Core Rendering (rendering)
  38. 2. List Performance (list-performance)
  39. 3. Animation (animation)
  40. 4. Scroll Performance (scroll)
  41. 5. Navigation (navigation)
  42. 6. React State (react-state)
  43. 7. State Architecture (state)
  44. 8. React Compiler (react-compiler)
  45. 9. User Interface (ui)
  46. 10. Design System (design-system)
  47. 11. Monorepo (monorepo)
  48. 12. Third-Party Dependencies (imports)
  49. 13. JavaScript (js)
  50. 14. Fonts (fonts)
  51. rules/template.md (verbatim)
  52. Rule Title Here
  53. rules/animation-derived-value.md (verbatim)
  54. Prefer useDerivedValue Over useAnimatedReaction
  55. rules/animation-gesture-detector-press.md (verbatim)
  56. Use GestureDetector for Animated Press States
  57. rules/animation-gpu-properties.md (verbatim)
  58. Animate Transform and Opacity Instead of Layout Properties
  59. rules/design-system-compound-components.md (verbatim)
  60. Use Compound Components Over Polymorphic Children
  61. rules/fonts-config-plugin.md (verbatim)
  62. Use Expo Config Plugin for Font Loading
  63. rules/imports-design-system-folder.md (verbatim)
  64. Import from Design System Folder
  65. rules/js-hoist-intl.md (verbatim)
  66. Hoist Intl Formatter Creation
  67. rules/list-performance-callbacks.md (verbatim)
  68. List performance callbacks
  69. rules/list-performance-function-references.md (verbatim)
  70. Optimize List Performance with Stable Object References
  71. rules/list-performance-images.md (verbatim)
  72. Use Compressed Images in Lists
  73. rules/list-performance-inline-objects.md (verbatim)
  74. Avoid Inline Objects in renderItem
  75. rules/list-performance-item-expensive.md (verbatim)
  76. Keep List Items Lightweight
  77. rules/list-performance-item-memo.md (verbatim)
  78. Pass Primitives to List Items for Memoization
  79. rules/list-performance-item-types.md (verbatim)
  80. Use Item Types for Heterogeneous Lists
  81. rules/list-performance-virtualize.md (verbatim)
  82. Use a List Virtualizer for Any List
  83. rules/monorepo-native-deps-in-app.md (verbatim)
  84. Install Native Dependencies in App Directory
  85. rules/monorepo-single-dependency-versions.md (verbatim)
  86. Use Single Dependency Versions Across Monorepo
  87. rules/navigation-native-navigators.md (verbatim)
  88. Use Native Navigators for Navigation
  89. Stack Navigation
  90. Tab Navigation
  91. Prefer Native Header Options Over Custom Components
  92. Why Native Navigators
  93. rules/react-compiler-destructure-functions.md (verbatim)
  94. Destructure Functions Early in Render

What it does. 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. Part of vercel-labs/agent-skills (Vercel official skills) (vercel-labs/agent-skills).

Upstream vercel-labs/agent-skills
Skill file skills/react-native-skills/SKILL.md
License MIT (stated in the README; no LICENSE file)
Author Vercel Labs
Fetched 2026-09-10

Install

  • npx skills add vercel-labs/agent-skills --skill react-native-skills, or copy the skill folder into ~/.claude/skills/react-native-skills/.
  • Raw file: curl -sL https://raw.githubusercontent.com/vercel-labs/agent-skills/HEAD/skills/react-native-skills/SKILL.md

SKILL.md (verbatim)

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

Other files in this skill

README.md (verbatim)

React Native Guidelines

A structured repository for creating and maintaining React Native Best Practices optimized for agents and LLMs.

Structure

  • rules/ - Individual rule files (one per rule)
    • _sections.md - Section metadata (titles, impacts, descriptions)
    • _template.md - Template for creating new rules
    • area-description.md - Individual rule files
  • metadata.json - Document metadata (version, organization, abstract)
  • AGENTS.md - Compiled output (generated)

Rules

Core Rendering (CRITICAL)

  • rendering-text-in-text-component.md - Wrap strings in Text components
  • rendering-no-falsy-and.md - Avoid falsy && operator in JSX

List Performance (HIGH)

  • list-performance-virtualize.md - Use virtualized lists (LegendList, FlashList)
  • list-performance-function-references.md - Keep stable object references
  • list-performance-callbacks.md - Hoist callbacks to list root
  • list-performance-inline-objects.md - Avoid inline objects in renderItem
  • list-performance-item-memo.md - Pass primitives for memoization
  • list-performance-item-expensive.md - Keep list items lightweight
  • list-performance-images.md - Use compressed images in lists
  • list-performance-item-types.md - Use item types for heterogeneous lists

Animation (HIGH)

  • animation-gpu-properties.md - Animate transform/opacity instead of layout
  • animation-gesture-detector-press.md - Use GestureDetector for press animations
  • animation-derived-value.md - Prefer useDerivedValue over useAnimatedReaction

Scroll Performance (HIGH)

  • scroll-position-no-state.md - Never track scroll in useState
  • navigation-native-navigators.md - Use native stack and native tabs

React State (MEDIUM)

  • react-state-dispatcher.md - Use functional setState updates
  • react-state-fallback.md - State should represent user intent only
  • react-state-minimize.md - Minimize state variables, derive values

State Architecture (MEDIUM)

  • state-ground-truth.md - State must represent ground truth

React Compiler (MEDIUM)

  • react-compiler-destructure-functions.md - Destructure functions early
  • react-compiler-reanimated-shared-values.md - Use .get()/.set() for shared values

User Interface (MEDIUM)

  • ui-expo-image.md - Use expo-image for optimized images
  • ui-image-gallery.md - Use Galeria for lightbox/galleries
  • ui-menus.md - Native dropdown and context menus with Zeego
  • ui-native-modals.md - Use native Modal with formSheet
  • ui-pressable.md - Use Pressable instead of TouchableOpacity
  • ui-measure-views.md - Measuring view dimensions
  • ui-safe-area-scroll.md - Use contentInsetAdjustmentBehavior
  • ui-scrollview-content-inset.md - Use contentInset for dynamic spacing
  • ui-styling.md - Modern styling patterns (gap, boxShadow, gradients)

Design System (MEDIUM)

  • design-system-compound-components.md - Use compound components

Monorepo (LOW)

  • monorepo-native-deps-in-app.md - Install native deps in app directory
  • monorepo-single-dependency-versions.md - Single dependency versions

Third-Party Dependencies (LOW)

  • imports-design-system-folder.md - Import from design system folder

JavaScript (LOW)

  • js-hoist-intl.md - Hoist Intl formatter creation

Fonts (LOW)

  • fonts-config-plugin.md - Load fonts natively at build time

Creating a New Rule

  1. Copy rules/_template.md to rules/area-description.md
  2. Choose the appropriate area prefix:
    • rendering- for Core Rendering
    • list-performance- for List Performance
    • animation- for Animation
    • scroll- for Scroll Performance
    • navigation- for Navigation
    • react-state- for React State
    • state- for State Architecture
    • react-compiler- for React Compiler
    • ui- for User Interface
    • design-system- for Design System
    • monorepo- for Monorepo
    • imports- for Third-Party Dependencies
    • js- for JavaScript
    • fonts- for Fonts
  3. Fill in the frontmatter and content
  4. Ensure you have clear examples with explanations

Rule File Structure

Each rule file should follow this structure:

---
title: Rule Title Here
impact: MEDIUM
impactDescription: Optional description
tags: tag1, tag2, tag3
---

## Rule Title Here

Brief explanation of the rule and why it matters.

**Incorrect (description of what's wrong):**

```tsx
// Bad code example
```

Correct (description of what's right):

// Good code example

Reference: Link


## File Naming Convention

- Files starting with `_` are special (excluded from build)
- Rule files: `area-description.md` (e.g., `animation-gpu-properties.md`)
- Section is automatically inferred from filename prefix
- Rules are sorted alphabetically by title within each section

## Impact Levels

- `CRITICAL` - Highest priority, causes crashes or broken UI
- `HIGH` - Significant performance improvements
- `MEDIUM` - Moderate performance improvements
- `LOW` - Incremental improvements

rules/_sections.md (verbatim)

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.

rules/_template.md (verbatim)


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):

// Bad code example here
const bad = example()

Correct (description of what's right):

// Good code example here
const good = example()

Reference: Link to documentation or resource

rules/animation-derived-value.md (verbatim)


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):

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):

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

rules/animation-gesture-detector-press.md (verbatim)


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):

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):

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

rules/animation-gpu-properties.md (verbatim)


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):

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):

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):

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.

rules/design-system-compound-components.md (verbatim)


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):

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):

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>

rules/fonts-config-plugin.md (verbatim)


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):

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):

// app.json
{
  "expo": {
    "plugins": [
      [
        "expo-font",
        {
          "fonts": ["./assets/fonts/Geist-Bold.otf"]
        }
      ]
    ]
  }
}
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

rules/imports-design-system-folder.md (verbatim)


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):

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):

// 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} />
}
// components/text.tsx
export { Text } from 'react-native'
// components/button.tsx
export { Button } from '@ui/button'
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.

rules/js-hoist-intl.md (verbatim)


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):

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):

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:

const dateFormatter = useMemo(
  () => new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }),
  [locale]
)

Common formatters to hoist:

// 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.

rules/list-performance-callbacks.md (verbatim)


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):

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):

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

rules/list-performance-function-references.md (verbatim)


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):

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):

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:

// 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):

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:

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.

rules/list-performance-images.md (verbatim)


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):

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):

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.

rules/list-performance-inline-objects.md (verbatim)


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):

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):

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):

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):

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):

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.

rules/list-performance-item-expensive.md (verbatim)


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):

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):

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:

// 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:

// 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.

rules/list-performance-item-memo.md (verbatim)


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):

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):

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:

// 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:

// 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.

rules/list-performance-item-types.md (verbatim)


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):

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):

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
<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

rules/list-performance-virtualize.md (verbatim)


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):

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):

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):

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.

rules/monorepo-native-deps-in-app.md (verbatim)


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
// 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.

rules/monorepo-single-dependency-versions.md (verbatim)


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):

// 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):

// 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 ~.

rules/navigation-native-navigators.md (verbatim)


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):

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):

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):

// app/_layout.tsx
import { Stack } from 'expo-router'

export default function Layout() {
  return <Stack />
}

Tab Navigation

Incorrect (JS bottom tabs):

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):

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):

// 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):

<Stack.Screen
  name='Profile'
  component={ProfileScreen}
  options={{
    header: () => <CustomHeader title='Profile' />,
  }}
/>

Correct (native header options):

<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:

rules/react-compiler-destructure-functions.md (verbatim)


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):

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):

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>
}

Back to vercel-labs/agent-skills (Vercel official skills) or Agent skills.