vercel-react-native-skills
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.
Install
npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-react-native-skillsReact Native Skills
Version 1.0.0
Engineering
January 2026
Note:
This document is mainly for agents and LLMs to follow when maintaining,
generating, or refactoring React Native codebases. Humans
may also find it useful, but guidance here is optimized for automation
and consistency by AI-assisted workflows.
Abstract
Comprehensive performance optimization guide for React Native applications, designed for AI agents and LLMs. Contains 35+ rules across 13 categories, prioritized by impact from critical (core rendering, list performance) to incremental (fonts, imports). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.
Table of Contents
- Core Rendering — CRITICAL
- List Performance — HIGH
- 2.1 Avoid Inline Objects in renderItem
- 2.2 Hoist callbacks to the root of lists
- 2.3 Keep List Items Lightweight
- 2.4 Optimize List Performance with Stable Object References
- 2.5 Pass Primitives to List Items for Memoization
- 2.6 Use a List Virtualizer for Any List
- 2.7 Use Compressed Images in Lists
- 2.8 Use Item Types for Heterogeneous Lists
- Animation — HIGH
- Scroll Performance — HIGH
- Navigation — HIGH
- React State — MEDIUM
- State Architecture — MEDIUM
- React Compiler — MEDIUM
- User Interface — MEDIUM
- 9.1 Measuring View Dimensions
- 9.2 Modern React Native Styling Patterns
- 9.3 Use contentInset for Dynamic ScrollView Spacing
- 9.4 Use contentInsetAdjustmentBehavior for Safe Areas
- 9.5 Use expo-image for Optimized Images
- 9.6 Use Galeria for Image Galleries and Lightbox
- 9.7 Use Native Menus for Dropdowns and Context Menus
- 9.8 Use Native Modals Over JS-Based Bottom Sheets
- 9.9 Use Pressable Instead of Touchable Components
- Design System — MEDIUM
- Monorepo — LOW
- 11.1 Install Native Dependencies in App Directory
- 11.2 Use Single Dependency Versions Across Monorepo
- Third-Party Dependencies — LOW
- JavaScript — LOW
- Fonts — LOW
1. Core Rendering
Impact: CRITICAL
Fundamental React Native rendering rules. Violations cause runtime crashes or broken UI.
1.1 Never Use && with Potentially Falsy Values
Impact: CRITICAL (prevents production crash)
Never use {value && <Component />} when value could be an empty string or
0. These are falsy but JSX-renderable—React Native will try to render them as
text outside a <Text> component, causing a hard crash in production.
Incorrect: crashes if count is 0 or name is ""
Correct: ternary with null
Correct: explicit boolean coercion
Best: early return
Early returns are clearest. When using conditionals inline, prefer ternary or
explicit boolean checks.
Lint rule: Enable react/jsx-no-leaked-render from
to catch this automatically.
1.2 Wrap Strings in Text Components
Impact: CRITICAL (prevents runtime crash)
Strings must be rendered inside <Text>. React Native crashes if a string is a
direct child of <View>.
Incorrect: crashes
Correct:
2. List Performance
Impact: HIGH
Optimizing virtualized lists (FlatList, LegendList, FlashList) for smooth scrolling and fast updates.
2.1 Avoid Inline Objects in renderItem
Impact: HIGH (prevents unnecessary re-renders of memoized list items)
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
Incorrect: inline style object
Correct: pass item directly or primitives
Correct: pass primitives, derive inside child
Correct: hoist static styles in module scope
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.
2.2 Hoist callbacks to the root of lists
Impact: MEDIUM (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
Correct: a single function instance passed to each item
Reference: https://example.com
2.3 Keep List Items Lightweight
Impact: HIGH (reduces render time for visible items during scroll)
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
Correct: lightweight list item
Move data fetching to parent:
For shared values, use Zustand selectors instead of Context:
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.
2.4 Optimize List Performance with Stable Object References
Impact: CRITICAL (virtualization relies on reference stability)
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
Correct: stable references, transform inside items
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:
Even though this creates a new array instance sortedTlds, the inner object
references are stable.
With zustand for dynamic data: avoids parent re-renders
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:
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.
2.5 Pass Primitives to List Items for Memoization
Impact: HIGH (enables effective memo() comparison)
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
This can still be optimized, but it is harder to memoize properly.
Correct: primitive props enable shallow comparison
Pass only what you need:
For callbacks, hoist or use item ID:
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.
2.6 Use a List Virtualizer for Any List
Impact: HIGH (reduced memory, faster mounts)
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
Correct: virtualizer renders only visible items
Alternative: FlashList
Benefits apply to any screen with scrollable content—profiles, settings, feeds,
search results. Default to virtualization.
2.7 Use Compressed Images in Lists
Impact: HIGH (faster load times, less memory)
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
Correct: request appropriately-sized image
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.
2.8 Use Item Types for Heterogeneous Lists
Impact: HIGH (efficient recycling, less layout thrashing)
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
Correct: typed items with separate components
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
getEstimatedItemSizewithitemTypeforaccurate estimates per type
3. Animation
Impact: HIGH
GPU-accelerated animations, Reanimated patterns, and avoiding render thrashing during gestures.
3.1 Animate Transform and Opacity Instead of Layout Properties
Impact: HIGH (GPU-accelerated animations, no layout recalculation)
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
Correct: animates scaleY, GPU-accelerated
Correct: animates translateY for slide animations
GPU-accelerated properties: transform (translate, scale, rotate), opacity. Everything else triggers layout.
3.2 Prefer useDerivedValue Over useAnimatedReaction
Impact: MEDIUM (cleaner code, automatic dependency tracking)
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
Correct: useDerivedValue
Use useAnimatedReaction only for side effects that don't produce a value
(e.g., triggering haptics, logging, calling runOnJS).
3.3 Use GestureDetector for Animated Press States
Impact: MEDIUM (UI thread animations, smoother press feedback)
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
Correct: GestureDetector with UI thread worklets
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.
4. Scroll Performance
Impact: HIGH
Tracking scroll position without causing render thrashing.
4.1 Never Track Scroll Position in useState
Impact: HIGH (prevents render thrashing during scroll)
Never store scroll position in useState. Scroll events fire rapidly—state
updates cause render thrashing and dropped frames. Use a Reanimated shared value
for animations or a ref for non-reactive tracking.
Incorrect: useState causes jank
Correct: Reanimated for animations
Correct: ref for non-reactive tracking
5. Navigation
Impact: HIGH
Using native navigators for stack and tab navigation instead of JS-based alternatives.
5.1 Use Native Navigators for Navigation
Impact: HIGH (native performance, platform-appropriate UI)
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.
- React Navigation Native Stack
- React Native Bottom Tabs with React Navigation
- React Native Bottom Tabs with Expo Router
- Expo Router Native Tabs
Incorrect: JS stack navigator
Correct: native stack with react-navigation
Correct: expo-router uses native stack by default
Incorrect: JS bottom tabs
Correct: native bottom tabs with react-navigation
Correct: expo-router native tabs
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.
Incorrect: custom header component
Correct: native header options
Native headers support iOS large titles, search bars, blur effects, and proper
safe area handling automatically.
- 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
6. React State
Impact: MEDIUM
Patterns for managing React state to avoid stale closures and unnecessary re-renders.
6.1 Minimize State Variables and Derive Values
Impact: MEDIUM (fewer re-renders, less state drift)
Use the fewest state variables possible. If a value can be computed from existing state or props, derive it during render instead of storing it in state. Redundant state causes unnecessary re-renders and can drift out of sync.
Incorrect: redundant state
Correct: derived values
Another example:
State should be the minimal source of truth. Everything else is derived.
Reference: https://react.dev/learn/choosing-the-state-structure
6.2 Use fallback state instead of initialState
Impact: MEDIUM (reactive fallbacks without syncing)
Use undefined as initial state and nullish coalescing (??) to fall back to
parent or server values. State represents user intent only—undefined means
"user hasn't chosen yet." This enables reactive fallbacks that update when the
source changes, not just on initial render.
Incorrect: syncs state, loses reactivity
Correct: state is user intent, reactive fallback
With server data:
6.3 useState Dispatch updaters for State That Depends on Current Value
Impact: MEDIUM (avoids stale closures, prevents unnecessary re-renders)
When the next state depends on the current state, use a dispatch updater
(setState(prev => ...)) instead of reading the state variable directly in a
callback. This avoids stale closures and ensures you're comparing against the
latest value.
Incorrect: reads state directly
Correct: dispatch updater
Returning the previous value from the updater skips the re-render.
For primitive states, you don't need to compare values before firing a
re-render.
Incorrect: unnecessary comparison for primitive state
Correct: sets primitive state directly
However, if the next state depends on the current state, you should still use a
dispatch updater.
Incorrect: reads state directly from the callback
Correct: dispatch updater
7. State Architecture
Impact: MEDIUM
Ground truth principles for state variables and derived values.
7.1 State Must Represent Ground Truth
Impact: HIGH (cleaner logic, easier debugging, single source of truth)
State variables—both React useState and Reanimated shared values—should
represent the actual state of something (e.g., pressed, progress, isOpen),
not derived visual values (e.g., scale, opacity, translateY). Derive
visual values from state using computation or interpolation.
Incorrect: storing the visual output
Correct: storing the state, deriving the visual
Why this matters:
State variables should represent real "state", not necessarily a desired end
result.
-
Single source of truth — The state (
pressed) describes what'shappening; visuals are derived
-
Easier to extend — Adding opacity, rotation, or other effects just
requires more interpolations from the same state
- Debugging — Inspecting
pressed = 1is clearer thanscale = 0.95 -
Reusable logic — The same
pressedvalue can drive multiple visualproperties
Same principle for React state:
State is the minimal truth. Everything else is derived.
8. React Compiler
Impact: MEDIUM
Compatibility patterns for React Compiler with React Native and Reanimated.
8.1 Destructure Functions Early in Render (React Compiler)
Impact: HIGH (stable references, fewer re-renders)
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
Correct: destructure early
8.2 Use .get() and .set() for Reanimated Shared Values (not .value)
Impact: LOW (required for React Compiler compatibility)
With React Compiler enabled, use .get() and .set() instead of reading or
writing .value directly on Reanimated shared values. The compiler can't track
property access—explicit methods ensure correct behavior.
Incorrect: breaks with React Compiler
Correct: React Compiler compatible
See the
for more.
9. User Interface
Impact: MEDIUM
Native UI patterns for images, menus, modals, styling, and platform-consistent interfaces.
9.1 Measuring View Dimensions
Impact: MEDIUM (synchronous measurement, avoid unnecessary re-renders)
Use both useLayoutEffect (synchronous) and onLayout (for updates). The sync
measurement gives you the initial size immediately; onLayout keeps it current
when the view changes. For non-primitive states, use a dispatch updater to
compare values and avoid unnecessary re-renders.
Height only:
Both dimensions:
Use functional setState to compare—don't read state directly in the callback.
9.2 Modern React Native Styling Patterns
Impact: MEDIUM (consistent design, smoother borders, cleaner layouts)
Follow these styling patterns for cleaner, more consistent React Native code.
Always use borderCurve: 'continuous' with borderRadius:
Use gap instead of margin for spacing between elements:
Use padding for space within, gap for space between:
Use experimental_backgroundImage for linear gradients:
Use CSS boxShadow string syntax for shadows:
Avoid multiple font sizes – use weight and color for emphasis:
Limiting font sizes creates visual consistency. Use fontWeight (bold/semibold)
and grayscale colors for hierarchy instead.
9.3 Use contentInset for Dynamic ScrollView Spacing
Impact: LOW (smoother updates, no layout recalculation)
When adding space to the top or bottom of a ScrollView that may change
(keyboard, toolbars, dynamic content), use contentInset instead of padding.
Changing contentInset doesn't trigger layout recalculation—it adjusts the
scroll area without re-rendering content.
Incorrect: padding causes layout recalculation
Correct: contentInset for dynamic spacing
Use scrollIndicatorInsets alongside contentInset to keep the scroll
indicator aligned. For static spacing that never changes, padding is fine.
9.4 Use contentInsetAdjustmentBehavior for Safe Areas
Impact: MEDIUM (native safe area handling, no layout shifts)
Use contentInsetAdjustmentBehavior="automatic" on the root ScrollView instead of wrapping content in SafeAreaView or manual padding. This lets iOS handle safe area insets natively with proper scroll behavior.
Incorrect: SafeAreaView wrapper
Incorrect: manual safe area padding
Correct: native content inset adjustment
The native approach handles dynamic safe areas (keyboard, toolbars) and allows content to scroll behind the status bar naturally.
9.5 Use expo-image for Optimized Images
Impact: HIGH (memory efficiency, caching, blurhash placeholders, progressive loading)
Use expo-image instead of React Native's Image. It provides memory-efficient caching, blurhash placeholders, progressive loading, and better performance for lists.
Incorrect: React Native Image
Correct: expo-image
With blurhash placeholder:
With priority and caching:
Key props:
placeholder— Blurhash or thumbnail while loadingcontentFit—cover,contain,fill,scale-downtransition— Fade-in duration (ms)priority—low,normal,highcachePolicy—memory,disk,memory-disk,nonerecyclingKey— Unique key for list recycling
For cross-platform (web + native), use SolitoImage from solito/image which uses expo-image under the hood.
Reference: https://docs.expo.dev/versions/latest/sdk/image/
9.6 Use Galeria for Image Galleries and Lightbox
Impact: MEDIUM
For image galleries with lightbox (tap to fullscreen), use @nandorojo/galeria.
It provides native shared element transitions with pinch-to-zoom, double-tap
zoom, and pan-to-close. Works with any image component including expo-image.
Incorrect: custom modal implementation
Correct: Galeria with expo-image
Single image:
With low-res thumbnails and high-res fullscreen:
With FlashList:
Works with expo-image, SolitoImage, react-native Image, or any image
component.
Reference: https://github.com/nandorojo/galeria
9.7 Use Native Menus for Dropdowns and Context Menus
Impact: HIGH (native accessibility, platform-consistent UX)
Use native platform menus instead of custom JS implementations. Native menus
provide built-in accessibility, consistent platform UX, and better performance.
Use zeego for cross-platform native menus.
Incorrect: custom JS menu
Correct: native menu with zeego
Context menu: long-press
Checkbox items:
Submenus:
Reference: https://zeego.dev/components/dropdown-menu
9.8 Use Native Modals Over JS-Based Bottom Sheets
Impact: HIGH (native performance, gestures, accessibility)
Use native <Modal> with presentationStyle="formSheet" or React Navigation
v7's native form sheet instead of JS-based bottom sheet libraries. Native modals
have built-in gestures, accessibility, and better performance. Rely on native UI
for low-level primitives.
Incorrect: JS-based bottom sheet
Correct: native Modal with formSheet
Correct: React Navigation v7 native form sheet
Native modals provide swipe-to-dismiss, proper keyboard avoidance, and
accessibility out of the box.
9.9 Use Pressable Instead of Touchable Components
Impact: LOW (modern API, more flexible)
Never use TouchableOpacity or TouchableHighlight. Use Pressable from
react-native or react-native-gesture-handler instead.
Incorrect: legacy Touchable components
Correct: Pressable
Correct: Pressable from gesture handler for lists
Use react-native-gesture-handler Pressable inside scrollable lists for better
gesture coordination, as long as you are using the ScrollView from
react-native-gesture-handler as well.
For animated press states (scale, opacity changes): Use GestureDetector
with Reanimated shared values instead of Pressable's style callback. See the
animation-gesture-detector-press rule.
10. Design System
Impact: MEDIUM
Architecture patterns for building maintainable component libraries.
10.1 Use Compound Components Over Polymorphic Children
Impact: MEDIUM (flexible composition, clearer API)
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
Correct: compound components
11. Monorepo
Impact: LOW
Dependency management and native module configuration in monorepos.
11.1 Install Native Dependencies in App Directory
Impact: CRITICAL (required for autolinking to work)
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
Autolinking fails—native code not linked.
Correct: native dep in app directory
Even if the shared package uses the native dependency, the app must also list it
for autolinking to detect and link the native code.
11.2 Use Single Dependency Versions Across Monorepo
Impact: MEDIUM (avoids duplicate bundles, version conflicts)
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
Correct: exact versions, single source of truth
Use your package manager's override/resolution feature to enforce versions at
the root. When adding dependencies, specify exact versions without ^ or ~.
12. Third-Party Dependencies
Impact: LOW
Wrapping and re-exporting third-party dependencies for maintainability.
12.1 Import from Design System Folder
Impact: LOW (enables global changes and easy refactoring)
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
Correct: imports from design system
Start by simply re-exporting. Customize later without changing app code.
13. JavaScript
Impact: LOW
Micro-optimizations like hoisting expensive object creation.
13.1 Hoist Intl Formatter Creation
Impact: LOW-MEDIUM (avoids expensive object recreation)
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
Correct: hoisted to module scope
For dynamic locales, memoize:
Common formatters to hoist:
Creating Intl objects is significantly more expensive than RegExp or plain
objects—each instantiation parses locale data and builds internal lookup tables.
14. Fonts
Impact: LOW
Native font loading for improved performance.
14.1 Load fonts natively at build time
Impact: LOW (fonts available at launch, no async 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
Correct: config plugin, fonts embedded at build
After adding fonts to the config plugin, run npx expo prebuild and rebuild the
native app.