react-native-skills skill (vercel-labs/agent-skills)
- Install
- SKILL.md (verbatim)
- When to Apply
- Rule Categories by Priority
- Quick Reference
- 1. List Performance (CRITICAL)
- 2. Animation (HIGH)
- 3. Navigation (HIGH)
- 4. UI Patterns (HIGH)
- 5. State Management (MEDIUM)
- 6. Rendering (MEDIUM)
- 7. Monorepo (MEDIUM)
- 8. Configuration (LOW)
- How to Use
- Full Compiled Document
- Other files in this skill
- README.md (verbatim)
- Structure
- Rules
- Core Rendering (CRITICAL)
- List Performance (HIGH)
- Animation (HIGH)
- Scroll Performance (HIGH)
- Navigation (HIGH)
- React State (MEDIUM)
- State Architecture (MEDIUM)
- React Compiler (MEDIUM)
- User Interface (MEDIUM)
- Design System (MEDIUM)
- Monorepo (LOW)
- Third-Party Dependencies (LOW)
- JavaScript (LOW)
- Fonts (LOW)
- Creating a New Rule
- Rule File Structure
- rules/sections.md (verbatim)
- 1. Core Rendering (rendering)
- 2. List Performance (list-performance)
- 3. Animation (animation)
- 4. Scroll Performance (scroll)
- 5. Navigation (navigation)
- 6. React State (react-state)
- 7. State Architecture (state)
- 8. React Compiler (react-compiler)
- 9. User Interface (ui)
- 10. Design System (design-system)
- 11. Monorepo (monorepo)
- 12. Third-Party Dependencies (imports)
- 13. JavaScript (js)
- 14. Fonts (fonts)
- rules/template.md (verbatim)
- Rule Title Here
- rules/animation-derived-value.md (verbatim)
- Prefer useDerivedValue Over useAnimatedReaction
- rules/animation-gesture-detector-press.md (verbatim)
- Use GestureDetector for Animated Press States
- rules/animation-gpu-properties.md (verbatim)
- Animate Transform and Opacity Instead of Layout Properties
- rules/design-system-compound-components.md (verbatim)
- Use Compound Components Over Polymorphic Children
- rules/fonts-config-plugin.md (verbatim)
- Use Expo Config Plugin for Font Loading
- rules/imports-design-system-folder.md (verbatim)
- Import from Design System Folder
- rules/js-hoist-intl.md (verbatim)
- Hoist Intl Formatter Creation
- rules/list-performance-callbacks.md (verbatim)
- List performance callbacks
- rules/list-performance-function-references.md (verbatim)
- Optimize List Performance with Stable Object References
- rules/list-performance-images.md (verbatim)
- Use Compressed Images in Lists
- rules/list-performance-inline-objects.md (verbatim)
- Avoid Inline Objects in renderItem
- rules/list-performance-item-expensive.md (verbatim)
- Keep List Items Lightweight
- rules/list-performance-item-memo.md (verbatim)
- Pass Primitives to List Items for Memoization
- rules/list-performance-item-types.md (verbatim)
- Use Item Types for Heterogeneous Lists
- rules/list-performance-virtualize.md (verbatim)
- Use a List Virtualizer for Any List
- rules/monorepo-native-deps-in-app.md (verbatim)
- Install Native Dependencies in App Directory
- rules/monorepo-single-dependency-versions.md (verbatim)
- Use Single Dependency Versions Across Monorepo
- rules/navigation-native-navigators.md (verbatim)
- Use Native Navigators for Navigation
- Stack Navigation
- Tab Navigation
- Prefer Native Header Options Over Custom Components
- Why Native Navigators
- rules/react-compiler-destructure-functions.md (verbatim)
- 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 listslist-performance-item-memo- Memoize list item componentslist-performance-callbacks- Stabilize callback referenceslist-performance-inline-objects- Avoid inline style objectslist-performance-function-references- Extract functions outside renderlist-performance-images- Optimize images in listslist-performance-item-expensive- Move expensive work outside itemslist-performance-item-types- Use item types for heterogeneous lists
2. Animation (HIGH)
animation-gpu-properties- Animate only transform and opacityanimation-derived-value- Use useDerivedValue for computed animationsanimation-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 imagesui-image-gallery- Use Galeria for image lightboxesui-pressable- Use Pressable over TouchableOpacityui-safe-area-scroll- Handle safe areas in ScrollViewsui-scrollview-content-inset- Use contentInset for headersui-menus- Use native context menusui-native-modals- Use native modals when possibleui-measure-views- Use onLayout, not measure()ui-styling- Use StyleSheet.create or Nativewind
5. State Management (MEDIUM)
react-state-minimize- Minimize state subscriptionsreact-state-dispatcher- Use dispatcher pattern for callbacksreact-state-fallback- Show fallback on first renderreact-compiler-destructure-functions- Destructure for React Compilerreact-compiler-reanimated-shared-values- Handle shared values with compiler
6. Rendering (MEDIUM)
rendering-text-in-text-component- Wrap text in Text componentsrendering-no-falsy-and- Avoid falsy && for conditional rendering
7. Monorepo (MEDIUM)
monorepo-native-deps-in-app- Keep native dependencies in app packagemonorepo-single-dependency-versions- Use single versions across packages
8. Configuration (LOW)
fonts-config-plugin- Use config plugins for custom fontsimports-design-system-folder- Organize design system importsjs-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
- AGENTS.md
- README.md
- metadata.json
- rules/_sections.md
- rules/_template.md
- rules/animation-derived-value.md
- rules/animation-gesture-detector-press.md
- rules/animation-gpu-properties.md
- rules/design-system-compound-components.md
- rules/fonts-config-plugin.md
- rules/imports-design-system-folder.md
- rules/js-hoist-intl.md
- rules/list-performance-callbacks.md
- rules/list-performance-function-references.md
- rules/list-performance-images.md
- rules/list-performance-inline-objects.md
- rules/list-performance-item-expensive.md
- rules/list-performance-item-memo.md
- rules/list-performance-item-types.md
- rules/list-performance-virtualize.md
- rules/monorepo-native-deps-in-app.md
- rules/monorepo-single-dependency-versions.md
- rules/navigation-native-navigators.md
- rules/react-compiler-destructure-functions.md
- rules/react-compiler-reanimated-shared-values.md
- rules/react-state-dispatcher.md
- rules/react-state-fallback.md
- rules/react-state-minimize.md
- rules/rendering-no-falsy-and.md
- rules/rendering-text-in-text-component.md
- rules/scroll-position-no-state.md
- rules/state-ground-truth.md
- rules/ui-expo-image.md
- rules/ui-image-gallery.md
- rules/ui-measure-views.md
- rules/ui-menus.md
- rules/ui-native-modals.md
- rules/ui-pressable.md
- rules/ui-safe-area-scroll.md
- rules/ui-scrollview-content-inset.md
- rules/ui-styling.md
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 rulesarea-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 componentsrendering-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 referenceslist-performance-callbacks.md- Hoist callbacks to list rootlist-performance-inline-objects.md- Avoid inline objects in renderItemlist-performance-item-memo.md- Pass primitives for memoizationlist-performance-item-expensive.md- Keep list items lightweightlist-performance-images.md- Use compressed images in listslist-performance-item-types.md- Use item types for heterogeneous lists
Animation (HIGH)
animation-gpu-properties.md- Animate transform/opacity instead of layoutanimation-gesture-detector-press.md- Use GestureDetector for press animationsanimation-derived-value.md- Prefer useDerivedValue over useAnimatedReaction
Scroll Performance (HIGH)
scroll-position-no-state.md- Never track scroll in useState
Navigation (HIGH)
navigation-native-navigators.md- Use native stack and native tabs
React State (MEDIUM)
react-state-dispatcher.md- Use functional setState updatesreact-state-fallback.md- State should represent user intent onlyreact-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 earlyreact-compiler-reanimated-shared-values.md- Use .get()/.set() for shared values
User Interface (MEDIUM)
ui-expo-image.md- Use expo-image for optimized imagesui-image-gallery.md- Use Galeria for lightbox/galleriesui-menus.md- Native dropdown and context menus with Zeegoui-native-modals.md- Use native Modal with formSheetui-pressable.md- Use Pressable instead of TouchableOpacityui-measure-views.md- Measuring view dimensionsui-safe-area-scroll.md- Use contentInsetAdjustmentBehaviorui-scrollview-content-inset.md- Use contentInset for dynamic spacingui-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 directorymonorepo-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
- Copy
rules/_template.mdtorules/area-description.md - Choose the appropriate area prefix:
rendering-for Core Renderinglist-performance-for List Performanceanimation-for Animationscroll-for Scroll Performancenavigation-for Navigationreact-state-for React Statestate-for State Architecturereact-compiler-for React Compilerui-for User Interfacedesign-system-for Design Systemmonorepo-for Monorepoimports-for Third-Party Dependenciesjs-for JavaScriptfonts-for Fonts
- Fill in the frontmatter and content
- 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
getEstimatedItemSizewithitemTypefor 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:
- React Navigation Native Stack
- React Native Bottom Tabs with React Navigation
- React Native Bottom Tabs with Expo Router
- Expo Router Native Tabs
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.