git:20260126.483ff0d to git:20260126.e091729

2666 added, 2483 removed, shown as one replacement because the pair was too large to align, first 3,000 lines shown. Audit A to A.

# React 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
1. [Core Rendering](#1-core-rendering) — **CRITICAL**
- 1.1 [Never Use && with Potentially Falsy Values](#11-never-use--with-potentially-falsy-values)
- 1.2 [Wrap Strings in Text Components](#12-wrap-strings-in-text-components)
2. [List Performance](#2-list-performance) — **HIGH**
- 2.1 [Avoid Inline Objects in renderItem](#21-avoid-inline-objects-in-renderitem)
- 2.2 [Hoist callbacks to the root of lists](#22-hoist-callbacks-to-the-root-of-lists)
- 2.3 [Keep List Items Lightweight](#23-keep-list-items-lightweight)
- 2.4 [Optimize List Performance with Stable Object References](#24-optimize-list-performance-with-stable-object-references)
- 2.5 [Pass Primitives to List Items for Memoization](#25-pass-primitives-to-list-items-for-memoization)
- 2.6 [Use a List Virtualizer for Any List](#26-use-a-list-virtualizer-for-any-list)
- 2.7 [Use Compressed Images in Lists](#27-use-compressed-images-in-lists)
3. [Animation](#3-animation) — **HIGH**
- 3.1 [Animate Transform and Opacity Instead of Layout Properties](#31-animate-transform-and-opacity-instead-of-layout-properties)
- 3.2 [Prefer useDerivedValue Over useAnimatedReaction](#32-prefer-usederivedvalue-over-useanimatedreaction)
- 3.3 [Use GestureDetector for Animated Press States](#33-use-gesturedetector-for-animated-press-states)
4. [Scroll Performance](#4-scroll-performance) — **HIGH**
- 4.1 [Never Track Scroll Position in useState](#41-never-track-scroll-position-in-usestate)
- 5. [React State](#5-react-state) — **MEDIUM**
- - 5.1 [Minimize State Variables and Derive Values](#51-minimize-state-variables-and-derive-values)
- - 5.2 [Use fallback state instead of initialState](#52-use-fallback-state-instead-of-initialstate)
- - 5.3 [useState Dispatch updaters for State That Depends on Current Value](#53-usestate-dispatch-updaters-for-state-that-depends-on-current-value)
- 6. [State Architecture](#6-state-architecture) — **MEDIUM**
- - 6.1 [State Must Represent Ground Truth](#61-state-must-represent-ground-truth)
- 7. [React Compiler](#7-react-compiler) — **MEDIUM**
- - 7.1 [Destructure Functions Early in Render (React Compiler)](#71-destructure-functions-early-in-render-react-compiler)
- - 7.2 [Use .get() and .set() for Reanimated Shared Values (not .value)](#72-use-get-and-set-for-reanimated-shared-values-not-value)
- 8. [User Interface](#8-user-interface) — **MEDIUM**
- - 8.1 [Measuring View Dimensions](#81-measuring-view-dimensions)
- - 8.2 [Modern React Native Styling Patterns](#82-modern-react-native-styling-patterns)
- - 8.3 [Use contentInset for Dynamic ScrollView Spacing](#83-use-contentinset-for-dynamic-scrollview-spacing)
- - 8.4 [Use contentInsetAdjustmentBehavior for Safe Areas](#84-use-contentinsetadjustmentbehavior-for-safe-areas)
- - 8.5 [Use expo-image for Optimized Images](#85-use-expo-image-for-optimized-images)
- - 8.6 [Use Galeria for Image Galleries and Lightbox](#86-use-galeria-for-image-galleries-and-lightbox)
- - 8.7 [Use Native Menus for Dropdowns and Context Menus](#87-use-native-menus-for-dropdowns-and-context-menus)
- - 8.8 [Use Native Modals Over JS-Based Bottom Sheets](#88-use-native-modals-over-js-based-bottom-sheets)
- - 8.9 [Use Pressable Instead of Touchable Components](#89-use-pressable-instead-of-touchable-components)
- 9. [Design System](#9-design-system) — **MEDIUM**
- - 9.1 [Use Compound Components Over Polymorphic Children](#91-use-compound-components-over-polymorphic-children)
- 10. [Monorepo](#10-monorepo) — **LOW**
- - 10.1 [Install Native Dependencies in App Directory](#101-install-native-dependencies-in-app-directory)
- - 10.2 [Use Single Dependency Versions Across Monorepo](#102-use-single-dependency-versions-across-monorepo)
- 11. [Third-Party Dependencies](#11-third-party-dependencies) — **LOW**
- - 11.1 [Import from Design System Folder](#111-import-from-design-system-folder)
- 12. [JavaScript](#12-javascript) — **LOW**
- - 12.1 [Hoist Intl Formatter Creation](#121-hoist-intl-formatter-creation)
- 13. [Fonts](#13-fonts) — **LOW**
- - 13.1 [Load fonts natively at build time](#131-load-fonts-natively-at-build-time)
-
- ---
-
- ## 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 ""**
-
- ```tsx
- function Profile({ name, count }: { name: string; count: number }) {
- return (
- <View>
- {name && <Text>{name}</Text>}
- {count && <Text>{count} items</Text>}
- </View>
- )
- }
- // If name="" or count=0, renders the falsy value → crash
- ```
-
- **Correct: ternary with null**
-
- ```tsx
- function Profile({ name, count }: { name: string; count: number }) {
- return (
- <View>
- {name ? <Text>{name}</Text> : null}
- {count ? <Text>{count} items</Text> : null}
- </View>
- )
- }
- ```
-
- **Correct: explicit boolean coercion**
-
- ```tsx
- function Profile({ name, count }: { name: string; count: number }) {
- return (
- <View>
- {!!name && <Text>{name}</Text>}
- {!!count && <Text>{count} items</Text>}
- </View>
- )
- }
- ```
-
- **Best: early return**
-
- ```tsx
- function Profile({ name, count }: { name: string; count: number }) {
- if (!name) return null
-
- return (
- <View>
- <Text>{name}</Text>
- {count > 0 ? <Text>{count} items</Text> : null}
- </View>
- )
- }
- ```
-
- Early returns are clearest. When using conditionals inline, prefer ternary or
-
- explicit boolean checks.
-
- **Lint rule:** Enable `react/jsx-no-leaked-render` from
-
- [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-leaked-render.md)
-
- to catch this automatically.
-
- ### 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**
-
- ```tsx
- import { View } from 'react-native'
-
- function Greeting({ name }: { name: string }) {
- return <View>Hello, {name}!</View>
- }
- // Error: Text strings must be rendered within a <Text> component.
- ```
-
- **Correct:**
-
- ```tsx
- import { View, Text } from 'react-native'
-
- function Greeting({ name }: { name: string }) {
- return (
- <View>
- <Text>Hello, {name}!</Text>
- </View>
- )
- }
- ```
-
- ---
-
- ## 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**
-
- ```tsx
- function UserList({ users }: { users: User[] }) {
- return (
- <LegendList
- data={users}
- renderItem={({ item }) => (
- <UserRow
- // Bad: new object on every render
- user={{ id: item.id, name: item.name, avatar: item.avatar }}
- />
- )}
- />
- )
- }
- ```
-
- **Incorrect: inline style object**
-
- ```tsx
- renderItem={({ item }) => (
- <UserRow
- name={item.name}
- // Bad: new style object on every render
- style={{ backgroundColor: item.isActive ? 'green' : 'gray' }}
- />
- )}
- ```
-
- **Correct: pass item directly or primitives**
-
- ```tsx
- function UserList({ users }: { users: User[] }) {
- return (
- <LegendList
- data={users}
- renderItem={({ item }) => (
- // Good: pass the item directly
- <UserRow user={item} />
- )}
- />
- )
- }
- ```
-
- **Correct: pass primitives, derive inside child**
-
- ```tsx
- renderItem={({ item }) => (
- <UserRow
- id={item.id}
- name={item.name}
- isActive={item.isActive}
- />
- )}
-
- const UserRow = memo(function UserRow({ id, name, isActive }: Props) {
- // Good: derive style inside memoized component
- const backgroundColor = isActive ? 'green' : 'gray'
- return <View style={[styles.row, { backgroundColor }]}>{/* ... */}</View>
- })
- ```
-
- **Correct: hoist static styles in module scope**
-
- ```tsx
- const activeStyle = { backgroundColor: 'green' }
- const inactiveStyle = { backgroundColor: 'gray' }
-
- renderItem={({ item }) => (
- <UserRow
- name={item.name}
- // Good: stable references
- style={item.isActive ? activeStyle : inactiveStyle}
- />
- )}
- ```
-
- Passing primitives or stable references allows `memo()` to skip re-renders when
-
- the actual values haven't changed.
-
- **Note:** If you have the React Compiler enabled, it handles memoization
-
- automatically and these manual optimizations become less critical.
-
- ### 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**
-
- ```typescript
- return (
- <LegendList
- renderItem={({ item }) => {
- // bad: creates a new callback on each render
- const onPress = () => handlePress(item.id)
- return <Item key={item.id} item={item} onPress={onPress} />
- }}
- />
- )
- ```
-
- **Correct: a single function instance passed to each item**
-
- ```typescript
- const onPress = useCallback(() => handlePress(item.id), [handlePress, item.id])
-
- return (
- <LegendList
- renderItem={({ item }) => (
- <Item key={item.id} item={item} onPress={onPress} />
- )}
- />
- )
- ```
-
- Reference: [https://example.com](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**
-
- ```tsx
- function ProductRow({ id }: { id: string }) {
- // Bad: query inside list item
- const { data: product } = useQuery(['product', id], () => fetchProduct(id))
- // Bad: multiple context accesses
- const theme = useContext(ThemeContext)
- const user = useContext(UserContext)
- const cart = useContext(CartContext)
- // Bad: expensive computation
- const recommendations = useMemo(
- () => computeRecommendations(product),
- [product]
- )
-
- return <View>{/* ... */}</View>
- }
- ```
-
- **Correct: lightweight list item**
-
- ```tsx
- function ProductRow({ name, price, imageUrl }: Props) {
- // Good: receives only primitives, minimal hooks
- return (
- <View>
- <Image source={{ uri: imageUrl }} />
- <Text>{name}</Text>
- <Text>{price}</Text>
- </View>
- )
- }
- ```
-
- **Move data fetching to parent:**
-
- ```tsx
- // Parent fetches all data once
- function ProductList() {
- const { data: products } = useQuery(['products'], fetchProducts)
-
- return (
- <LegendList
- data={products}
- renderItem={({ item }) => (
- <ProductRow name={item.name} price={item.price} imageUrl={item.image} />
- )}
- />
- )
- }
- ```
-
- **For shared values, use Zustand selectors instead of Context:**
-
- ```tsx
- // Incorrect: Context causes re-render when any cart value changes
- function ProductRow({ id, name }: Props) {
- const { items } = useContext(CartContext)
- const inCart = items.includes(id)
- // ...
- }
-
- // Correct: Zustand selector only re-renders when this specific value changes
- function ProductRow({ id, name }: Props) {
- // use Set.has (created once at the root) instead of Array.includes()
- const inCart = useCartStore((s) => s.items.has(id))
- // ...
- }
- ```
-
- **Guidelines for list items:**
-
- - No queries or data fetching
-
- - No expensive computations (move to parent or memoize at parent level)
-
- - Prefer Zustand selectors over React Context
-
- - Minimize useState/useEffect hooks
-
- - Pass pre-computed values as props
-
- The goal: list items should be simple rendering functions that take props and
-
- return JSX.
-
- ### 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**
-
- ```tsx
- function DomainSearch() {
- const { keyword, setKeyword } = useKeywordZustandState()
- const { data: tlds } = useTlds()
-
- // Bad: creates new objects on every render, reparenting the entire list on every keystroke
- const domains = tlds.map((tld) => ({
- domain: `${keyword}.${tld.name}`,
- tld: tld.name,
- price: tld.price,
- }))
-
- return (
- <>
- <TextInput value={keyword} onChangeText={setKeyword} />
- <LegendList
- data={domains}
- renderItem={({ item }) => <DomainItem item={item} keyword={keyword} />}
- />
- </>
- )
- }
- ```
-
- **Correct: stable references, transform inside items**
-
- ```tsx
- const renderItem = ({ item }) => <DomainItem tld={item} />
-
- function DomainSearch() {
- const { data: tlds } = useTlds()
-
- return (
- <LegendList
- // good: as long as the data is stable, LegendList will not re-render the entire list
- data={tlds}
- renderItem={renderItem}
- />
- )
- }
-
- function DomainItem({ tld }: { tld: Tld }) {
- // good: transform within items, and don't pass the dynamic data as a prop
- // good: use a selector function from zustand to receive a stable string back
- const domain = useKeywordZustandState((s) => s.keyword + '.' + tld.name)
- return <Text>{domain}</Text>
- }
- ```
-
- **Updating parent array reference:**
-
- ```tsx
- // good: creates a new array instance without mutating the inner objects
- // good: parent array reference is unaffected by typing and updating "keyword"
- const sortedTlds = tlds.toSorted((a, b) => a.name.localeCompare(b.name))
-
- return <LegendList data={sortedTlds} renderItem={renderItem} />
- ```
-
- 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**
-
- ```tsx
- function DomainItemFavoriteButton({ tld }: { tld: Tld }) {
- const isFavorited = useFavoritesStore((s) => s.favorites.has(tld.id))
- return <TldFavoriteButton isFavorited={isFavorited} />
- }
- ```
-
- 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**
-
- ```tsx
- type User = { id: string; name: string; email: string; avatar: string }
-
- const UserRow = memo(function UserRow({ user }: { user: User }) {
- // memo() compares user by reference, not value
- // If parent creates new user object, this re-renders even if data is same
- return <Text>{user.name}</Text>
- })
-
- renderItem={({ item }) => <UserRow user={item} />}
- ```
-
- This can still be optimized, but it is harder to memoize properly.
-
- **Correct: primitive props enable shallow comparison**
-
- ```tsx
- const UserRow = memo(function UserRow({
- id,
- name,
- email,
- }: {
- id: string
- name: string
- email: string
- }) {
- // memo() compares each primitive directly
- // Re-renders only if id, name, or email actually changed
- return <Text>{name}</Text>
- })
-
- renderItem={({ item }) => (
- <UserRow id={item.id} name={item.name} email={item.email} />
- )}
- ```
-
- **Pass only what you need:**
-
- ```tsx
- // Incorrect: passing entire item when you only need name
- <UserRow user={item} />
-
- // Correct: pass only the fields the component uses
- <UserRow name={item.name} avatarUrl={item.avatar} />
- ```
-
- **For callbacks, hoist or use item ID:**
-
- ```tsx
- // Incorrect: inline function creates new reference
- <UserRow name={item.name} onPress={() => handlePress(item.id)} />
-
- // Correct: pass ID, handle in child
- <UserRow id={item.id} name={item.name} />
-
- const UserRow = memo(function UserRow({ id, name }: Props) {
- const handlePress = useCallback(() => {
- // use id here
- }, [id])
- return <Pressable onPress={handlePress}><Text>{name}</Text></Pressable>
- })
- ```
-
- Primitive props make memoization predictable and effective.
-
- **Note:** If you have the React Compiler enabled, you do not need to use
-
- `memo()` or `useCallback()`, but the object references still apply.
-
- ### 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**
-
- ```tsx
- function Feed({ items }: { items: Item[] }) {
- return (
- <ScrollView>
- {items.map((item) => (
- <ItemCard key={item.id} item={item} />
- ))}
- </ScrollView>
- )
- }
- // 50 items = 50 components mounted, even if only 10 visible
- ```
-
- **Correct: virtualizer renders only visible items**
-
- ```tsx
- import { LegendList } from '@legendapp/list'
-
- function Feed({ items }: { items: Item[] }) {
- return (
- <LegendList
- data={items}
- // if you aren't using React Compiler, wrap these with useCallback
- renderItem={({ item }) => <ItemCard item={item} />}
- keyExtractor={(item) => item.id}
- estimatedItemSize={80}
- />
- )
- }
- // Only ~10-15 visible items mounted at a time
- ```
-
- **Alternative: FlashList**
-
- ```tsx
- import { FlashList } from '@shopify/flash-list'
-
- function Feed({ items }: { items: Item[] }) {
- return (
- <FlashList
- data={items}
- // if you aren't using React Compiler, wrap these with useCallback
- renderItem={({ item }) => <ItemCard item={item} />}
- keyExtractor={(item) => item.id}
- />
- )
- }
- ```
-
- Benefits apply to any screen with scrollable content—profiles, settings, feeds,
-
- search results. Default to virtualization.
-
- ### 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**
-
- ```tsx
- function ProductItem({ product }: { product: Product }) {
- return (
- <View>
- {/* 4000x3000 image loaded for a 100x100 thumbnail */}
- <Image
- source={{ uri: product.imageUrl }}
- style={{ width: 100, height: 100 }}
- />
- <Text>{product.name}</Text>
- </View>
- )
- }
- ```
-
- **Correct: request appropriately-sized image**
-
- ```tsx
- function ProductItem({ product }: { product: Product }) {
- // Request a 200x200 image (2x for retina)
- const thumbnailUrl = `${product.imageUrl}?w=200&h=200&fit=cover`
-
- return (
- <View>
- <Image
- source={{ uri: thumbnailUrl }}
- style={{ width: 100, height: 100 }}
- contentFit='cover'
- />
- <Text>{product.name}</Text>
- </View>
- )
- }
- ```
-
- Use an optimized image component with built-in caching and placeholder support,
-
- such as `expo-image` or `SolitoImage` (which uses `expo-image` under the hood).
-
- Request images at 2x the display size for retina screens.
-
- ---
-
- ## 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**
-
- ```tsx
- import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
-
- function CollapsiblePanel({ expanded }: { expanded: boolean }) {
- const animatedStyle = useAnimatedStyle(() => ({
- height: withTiming(expanded ? 200 : 0), // triggers layout on every frame
- overflow: 'hidden',
- }))
-
- return <Animated.View style={animatedStyle}>{children}</Animated.View>
- }
- ```
-
- **Correct: animates scaleY, GPU-accelerated**
-
- ```tsx
- import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
-
- function CollapsiblePanel({ expanded }: { expanded: boolean }) {
- const animatedStyle = useAnimatedStyle(() => ({
- transform: [
- { scaleY: withTiming(expanded ? 1 : 0) },
- ],
- opacity: withTiming(expanded ? 1 : 0),
- }))
-
- return (
- <Animated.View style={[{ height: 200, transformOrigin: 'top' }, animatedStyle]}>
- {children}
- </Animated.View>
- )
- }
- ```
-
- **Correct: animates translateY for slide animations**
-
- ```tsx
- import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
-
- function SlideIn({ visible }: { visible: boolean }) {
- const animatedStyle = useAnimatedStyle(() => ({
- transform: [
- { translateY: withTiming(visible ? 0 : 100) },
- ],
- opacity: withTiming(visible ? 1 : 0),
- }))
-
- return <Animated.View style={animatedStyle}>{children}</Animated.View>
- }
- ```
-
- GPU-accelerated properties: `transform` (translate, scale, rotate), `opacity`. Everything else triggers layout.
-
- ### 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.
-
- [Reanimated useDerivedValue](https://docs.swmansion.com/react-native-reanimated/docs/core/useDerivedValue)
-
- **Incorrect: useAnimatedReaction for derivation**
-
- ```tsx
- import { useSharedValue, useAnimatedReaction } from 'react-native-reanimated'
-
- function MyComponent() {
- const progress = useSharedValue(0)
- const opacity = useSharedValue(1)
-
- useAnimatedReaction(
- () => progress.value,
- (current) => {
- opacity.value = 1 - current
- }
- )
-
- // ...
- }
- ```
-
- **Correct: useDerivedValue**
-
- ```tsx
- import { useSharedValue, useDerivedValue } from 'react-native-reanimated'
-
- function MyComponent() {
- const progress = useSharedValue(0)
-
- const opacity = useDerivedValue(() => 1 - progress.get())
-
- // ...
- }
- ```
-
- Use `useAnimatedReaction` only for side effects that don't produce a value
-
- (e.g., triggering haptics, logging, calling `runOnJS`).
-
- ### 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.
-
- [Gesture Handler Tap Gesture](https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture)
-
- **Incorrect: Pressable with JS thread callbacks**
-
- ```tsx
- import { Pressable } from 'react-native'
- import Animated, {
- useSharedValue,
- useAnimatedStyle,
- withTiming,
- } from 'react-native-reanimated'
-
- function AnimatedButton({ onPress }: { onPress: () => void }) {
- const scale = useSharedValue(1)
-
- const animatedStyle = useAnimatedStyle(() => ({
- transform: [{ scale: scale.value }],
- }))
-
- return (
- <Pressable
- onPress={onPress}
- onPressIn={() => (scale.value = withTiming(0.95))}
- onPressOut={() => (scale.value = withTiming(1))}
- >
- <Animated.View style={animatedStyle}>
- <Text>Press me</Text>
- </Animated.View>
- </Pressable>
- )
- }
- ```
-
- **Correct: GestureDetector with UI thread worklets**
-
- ```tsx
- import { Gesture, GestureDetector } from 'react-native-gesture-handler'
- import Animated, {
- useSharedValue,
- useAnimatedStyle,
- withTiming,
- interpolate,
- runOnJS,
- } from 'react-native-reanimated'
-
- function AnimatedButton({ onPress }: { onPress: () => void }) {
- // Store the press STATE (0 = not pressed, 1 = pressed)
- const pressed = useSharedValue(0)
-
- const tap = Gesture.Tap()
- .onBegin(() => {
- pressed.set(withTiming(1))
- })
- .onFinalize(() => {
- pressed.set(withTiming(0))
- })
- .onEnd(() => {
- runOnJS(onPress)()
- })
-
- // Derive visual values from the state
- const animatedStyle = useAnimatedStyle(() => ({
- transform: [
- { scale: interpolate(withTiming(pressed.get()), [0, 1], [1, 0.95]) },
- ],
- }))
-
- return (
- <GestureDetector gesture={tap}>
- <Animated.View style={animatedStyle}>
- <Text>Press me</Text>
- </Animated.View>
- </GestureDetector>
- )
- }
- ```
-
- Store the press **state** (0 or 1), then derive the scale via `interpolate`.
-
- This keeps the shared value as ground truth. Use `runOnJS` to call JS functions
-
- from worklets. Use `.set()` and `.get()` for React Compiler compatibility.
-
- ---
-
- ## 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**
-
- ```tsx
- import { useState } from 'react'
- import {
- ScrollView,
- NativeSyntheticEvent,
- NativeScrollEvent,
- } from 'react-native'
-
- function Feed() {
- const [scrollY, setScrollY] = useState(0)
-
- const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
- setScrollY(e.nativeEvent.contentOffset.y) // re-renders on every frame
- }
-
- return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />
- }
- ```
-
- **Correct: Reanimated for animations**
-
- ```tsx
- import Animated, {
- useSharedValue,
- useAnimatedScrollHandler,
- } from 'react-native-reanimated'
-
- function Feed() {
- const scrollY = useSharedValue(0)
-
- const onScroll = useAnimatedScrollHandler({
- onScroll: (e) => {
- scrollY.value = e.contentOffset.y // runs on UI thread, no re-render
- },
- })
-
- return (
- <Animated.ScrollView
- onScroll={onScroll}
- // higher number has better performance, but it fires less often.
- // unset this if you need higher precision over performance.
- scrollEventThrottle={16}
- />
- )
- }
- ```
-
- **Correct: ref for non-reactive tracking**
-
- ```tsx
- import { useRef } from 'react'
- import {
- ScrollView,
- NativeSyntheticEvent,
- NativeScrollEvent,
- } from 'react-native'
-
- function Feed() {
- const scrollY = useRef(0)
-
- const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
- scrollY.current = e.nativeEvent.contentOffset.y // no re-render
- }
-
- return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />
- }
- ```
-
- ---
-
- ## 5. React State
-
- **Impact: MEDIUM**
-
- Patterns for managing React state to avoid stale closures and
- unnecessary re-renders.
-
- ### 5.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**
-
- ```tsx
- function Cart({ items }: { items: Item[] }) {
- const [total, setTotal] = useState(0)
- const [itemCount, setItemCount] = useState(0)
-
- useEffect(() => {
- setTotal(items.reduce((sum, item) => sum + item.price, 0))
- setItemCount(items.length)
- }, [items])
-
- return (
- <View>
- <Text>{itemCount} items</Text>
- <Text>Total: ${total}</Text>
- </View>
- )
- }
- ```
-
- **Correct: derived values**
-
- ```tsx
- function Cart({ items }: { items: Item[] }) {
- const total = items.reduce((sum, item) => sum + item.price, 0)
- const itemCount = items.length
-
- return (
- <View>
- <Text>{itemCount} items</Text>
- <Text>Total: ${total}</Text>
- </View>
- )
- }
- ```
-
- **Another example:**
-
- ```tsx
- // Incorrect: storing both firstName, lastName, AND fullName
- const [firstName, setFirstName] = useState('')
- const [lastName, setLastName] = useState('')
- const [fullName, setFullName] = useState('')
-
- // Correct: derive fullName
- const [firstName, setFirstName] = useState('')
- const [lastName, setLastName] = useState('')
- const fullName = `${firstName} ${lastName}`
- ```
-
- State should be the minimal source of truth. Everything else is derived.
-
- Reference: [https://react.dev/learn/choosing-the-state-structure](https://react.dev/learn/choosing-the-state-structure)
-
- ### 5.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**
-
- ```tsx
- type Props = { fallbackEnabled: boolean }
-
- function Toggle({ fallbackEnabled }: Props) {
- const [enabled, setEnabled] = useState(defaultEnabled)
- // If fallbackEnabled changes, state is stale
- // State mixes user intent with default value
-
- return <Switch value={enabled} onValueChange={setEnabled} />
- }
- ```
-
- **Correct: state is user intent, reactive fallback**
-
- ```tsx
- type Props = { fallbackEnabled: boolean }
-
- function Toggle({ fallbackEnabled }: Props) {
- const [_enabled, setEnabled] = useState<boolean | undefined>(undefined)
- const enabled = _enabled ?? defaultEnabled
- // undefined = user hasn't touched it, falls back to prop
- // If defaultEnabled changes, component reflects it
- // Once user interacts, their choice persists
-
- return <Switch value={enabled} onValueChange={setEnabled} />
- }
- ```
-
- **With server data:**
-
- ```tsx
- function ProfileForm({ data }: { data: User }) {
- const [_theme, setTheme] = useState<string | undefined>(undefined)
- const theme = _theme ?? data.theme
- // Shows server value until user overrides
- // Server refetch updates the fallback automatically
-
- return <ThemePicker value={theme} onChange={setTheme} />
- }
- ```
-
- ### 5.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**
-
- ```tsx
- const [size, setSize] = useState<Size | undefined>(undefined)
-
- const onLayout = (e: LayoutChangeEvent) => {
- const { width, height } = e.nativeEvent.layout
- // size may be stale in this closure
- if (size?.width !== width || size?.height !== height) {
- setSize({ width, height })
- }
- }
- ```
-
- **Correct: dispatch updater**
-
- ```tsx
- const [size, setSize] = useState<Size | undefined>(undefined)
-
- const onLayout = (e: LayoutChangeEvent) => {
- const { width, height } = e.nativeEvent.layout
- setSize((prev) => {
- if (prev?.width === width && prev?.height === height) return prev
- return { width, height }
- })
- }
- ```
-
- Returning the previous value from the updater skips the re-render.
-
- For primitive states, you don't need to compare values before firing a
-
- re-render.
-
- **Incorrect: unnecessary comparison for primitive state**
-
- ```tsx
- const [size, setSize] = useState<Size | undefined>(undefined)
-
- const onLayout = (e: LayoutChangeEvent) => {
- const { width, height } = e.nativeEvent.layout
- setSize((prev) => (prev === width ? prev : width))
- }
- ```
-
- **Correct: sets primitive state directly**
-
- ```tsx
- const [size, setSize] = useState<Size | undefined>(undefined)
-
- const onLayout = (e: LayoutChangeEvent) => {
- const { width, height } = e.nativeEvent.layout
- setSize(width)
- }
- ```
-
- However, if the next state depends on the current state, you should still use a
-
- dispatch updater.
-
- **Incorrect: reads state directly from the callback**
-
- ```tsx
- const [count, setCount] = useState(0)
-
- const onTap = () => {
- setCount(count + 1)
- }
- ```
-
- **Correct: dispatch updater**
-
- ```tsx
- const [count, setCount] = useState(0)
-
- const onTap = () => {
- setCount((prev) => prev + 1)
- }
- ```
-
- ---
-
- ## 6. State Architecture
-
- **Impact: MEDIUM**
-
- Ground truth principles for state variables and derived values.
-
- ### 6.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**
-
- ```tsx
- const scale = useSharedValue(1)
-
- const tap = Gesture.Tap()
- .onBegin(() => {
- scale.set(withTiming(0.95))
- })
- .onFinalize(() => {
- scale.set(withTiming(1))
- })
-
- const animatedStyle = useAnimatedStyle(() => ({
- transform: [{ scale: scale.get() }],
- }))
- ```
-
- **Correct: storing the state, deriving the visual**
-
- ```tsx
- const pressed = useSharedValue(0) // 0 = not pressed, 1 = pressed
-
- const tap = Gesture.Tap()
- .onBegin(() => {
- pressed.set(withTiming(1))
- })
- .onFinalize(() => {
- pressed.set(withTiming(0))
- })
-
- const animatedStyle = useAnimatedStyle(() => ({
- transform: [{ scale: interpolate(pressed.get(), [0, 1], [1, 0.95]) }],
- }))
- ```
-
- **Why this matters:**
-
- State variables should represent real "state", not necessarily a desired end
-
- result.
-
- 1. **Single source of truth** — The state (`pressed`) describes what's
-
- happening; visuals are derived
-
- 2. **Easier to extend** — Adding opacity, rotation, or other effects just
-
- requires more interpolations from the same state
-
- 3. **Debugging** — Inspecting `pressed = 1` is clearer than `scale = 0.95`
-
- 4. **Reusable logic** — The same `pressed` value can drive multiple visual
-
- properties
-
- **Same principle for React state:**
-
- ```tsx
- // Incorrect: storing derived values
- const [isExpanded, setIsExpanded] = useState(false)
- const [height, setHeight] = useState(0)
-
- useEffect(() => {
- setHeight(isExpanded ? 200 : 0)
- }, [isExpanded])
-
- // Correct: derive from state
- const [isExpanded, setIsExpanded] = useState(false)
- const height = isExpanded ? 200 : 0
- ```
-
- State is the minimal truth. Everything else is derived.
-
- ---
-
- ## 7. React Compiler
-
- **Impact: MEDIUM**
-
- Compatibility patterns for React Compiler with React Native and
- Reanimated.
-
- ### 7.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**
-
- ```tsx
- import { useRouter } from 'expo-router'
-
- function SaveButton(props) {
- const router = useRouter()
-
- // bad: react-compiler will key the cache on "props" and "router", which are objects that change each render
- const handlePress = () => {
- props.onSave()
- router.push('/success') // unstable reference
- }
-
- return <Button onPress={handlePress}>Save</Button>
- }
- ```
-
- **Correct: destructure early**
-
- ```tsx
- import { useRouter } from 'expo-router'
-
- function SaveButton({ onSave }) {
- const { push } = useRouter()
-
- // good: react-compiler will key on push and onSave
- const handlePress = () => {
- onSave()
- push('/success') // stable reference
- }
-
- return <Button onPress={handlePress}>Save</Button>
- }
- ```
-
- ### 7.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**
-
- ```tsx
- import { useSharedValue } from 'react-native-reanimated'
-
- function Counter() {
- const count = useSharedValue(0)
-
- const increment = () => {
- count.value = count.value + 1 // opts out of react compiler
- }
-
- return <Button onPress={increment} title={`Count: ${count.value}`} />
- }
- ```
-
- **Correct: React Compiler compatible**
-
- ```tsx
- import { useSharedValue } from 'react-native-reanimated'
-
- function Counter() {
- const count = useSharedValue(0)
-
- const increment = () => {
- count.set(count.get() + 1)
- }
-
- return <Button onPress={increment} title={`Count: ${count.get()}`} />
- }
- ```
-
- See the
-
- [Reanimated docs](https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support)
-
- for more.
-
- ---
-
- ## 8. User Interface
-
- **Impact: MEDIUM**
-
- Native UI patterns for images, menus, modals, styling, and
- platform-consistent interfaces.
-
- ### 8.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:**
-
- ```tsx
- import { useLayoutEffect, useRef, useState } from 'react'
- import { View, LayoutChangeEvent } from 'react-native'
-
- function MeasuredBox({ children }: { children: React.ReactNode }) {
- const ref = useRef<View>(null)
- const [height, setHeight] = useState<number | undefined>(undefined)
-
- useLayoutEffect(() => {
- // Sync measurement on mount (RN 0.82+)
- const rect = ref.current?.getBoundingClientRect()
- if (rect) setHeight(rect.height)
- // Pre-0.82: ref.current?.measure((x, y, w, h) => setHeight(h))
- }, [])
-
- const onLayout = (e: LayoutChangeEvent) => {
- setHeight(e.nativeEvent.layout.height)
- }
-
- return (
- <View ref={ref} onLayout={onLayout}>
- {children}
- </View>
- )
- }
- ```
-
- **Both dimensions:**
-
- ```tsx
- import { useLayoutEffect, useRef, useState } from 'react'
- import { View, LayoutChangeEvent } from 'react-native'
-
- type Size = { width: number; height: number }
-
- function MeasuredBox({ children }: { children: React.ReactNode }) {
- const ref = useRef<View>(null)
- const [size, setSize] = useState<Size | undefined>(undefined)
-
- useLayoutEffect(() => {
- const rect = ref.current?.getBoundingClientRect()
- if (rect) setSize({ width: rect.width, height: rect.height })
- }, [])
-
- const onLayout = (e: LayoutChangeEvent) => {
- const { width, height } = e.nativeEvent.layout
- setSize((prev) => {
- // for non-primitive states, compare values before firing a re-render
- if (prev?.width === width && prev?.height === height) return prev
- return { width, height }
- })
- }
-
- return (
- <View ref={ref} onLayout={onLayout}>
- {children}
- </View>
- )
- }
- ```
-
- Use functional setState to compare—don't read state directly in the callback.
-
- ### 8.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:**
-
- ```tsx
- // Incorrect – margin on children
- <View>
- <Text style={{ marginBottom: 8 }}>Title</Text>
- <Text style={{ marginBottom: 8 }}>Subtitle</Text>
- </View>
-
- // Correct – gap on parent
- <View style={{ gap: 8 }}>
- <Text>Title</Text>
- <Text>Subtitle</Text>
- </View>
- ```
-
- **Use `padding` for space within, `gap` for space between:**
-
- ```tsx
- <View style={{ padding: 16, gap: 12 }}>
- <Text>First</Text>
- <Text>Second</Text>
- </View>
- ```
-
- **Use `experimental_backgroundImage` for linear gradients:**
-
- ```tsx
- // Incorrect – third-party gradient library
- <LinearGradient colors={['#000', '#fff']} />
-
- // Correct – native CSS gradient syntax
- <View
- style={{
- experimental_backgroundImage: 'linear-gradient(to bottom, #000, #fff)',
- }}
- />
- ```
-
- **Use CSS `boxShadow` string syntax for shadows:**
-
- ```tsx
- // Incorrect – legacy shadow objects or elevation
- { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1 }
- { elevation: 4 }
-
- // Correct – CSS box-shadow syntax
- { boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' }
- ```
-
- **Avoid multiple font sizes – use weight and color for emphasis:**
-
- ```tsx
- // Incorrect – varying font sizes for hierarchy
- <Text style={{ fontSize: 18 }}>Title</Text>
- <Text style={{ fontSize: 14 }}>Subtitle</Text>
- <Text style={{ fontSize: 12 }}>Caption</Text>
-
- // Correct – consistent size, vary weight and color
- <Text style={{ fontWeight: '600' }}>Title</Text>
- <Text style={{ color: '#666' }}>Subtitle</Text>
- <Text style={{ color: '#999' }}>Caption</Text>
- ```
-
- Limiting font sizes creates visual consistency. Use `fontWeight` (bold/semibold)
-
- and grayscale colors for hierarchy instead.
-
- ### 8.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**
-
- ```tsx
- function Feed({ bottomOffset }: { bottomOffset: number }) {
- return (
- <ScrollView contentContainerStyle={{ paddingBottom: bottomOffset }}>
- {children}
- </ScrollView>
- )
- }
- // Changing bottomOffset triggers full layout recalculation
- ```
-
- **Correct: contentInset for dynamic spacing**
-
- ```tsx
- function Feed({ bottomOffset }: { bottomOffset: number }) {
- return (
- <ScrollView
- contentInset={{ bottom: bottomOffset }}
- scrollIndicatorInsets={{ bottom: bottomOffset }}
- >
- {children}
- </ScrollView>
- )
- }
- // Changing bottomOffset only adjusts scroll bounds
- ```
-
- Use `scrollIndicatorInsets` alongside `contentInset` to keep the scroll
-
- indicator aligned. For static spacing that never changes, padding is fine.
-
- ### 8.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**
-
- ```tsx
- import { SafeAreaView, ScrollView, View, Text } from 'react-native'
-
- function MyScreen() {
- return (
- <SafeAreaView style={{ flex: 1 }}>
- <ScrollView>
- <View>
- <Text>Content</Text>
- </View>
- </ScrollView>
- </SafeAreaView>
- )
- }
- ```
-
- **Incorrect: manual safe area padding**
-
- ```tsx
- import { ScrollView, View, Text } from 'react-native'
- import { useSafeAreaInsets } from 'react-native-safe-area-context'
-
- function MyScreen() {
- const insets = useSafeAreaInsets()
-
- return (
- <ScrollView contentContainerStyle={{ paddingTop: insets.top }}>
- <View>
- <Text>Content</Text>
- </View>
- </ScrollView>
- )
- }
- ```
-
- **Correct: native content inset adjustment**
-
- ```tsx
- import { ScrollView, View, Text } from 'react-native'
-
- function MyScreen() {
- return (
- <ScrollView contentInsetAdjustmentBehavior='automatic'>
- <View>
- <Text>Content</Text>
- </View>
- </ScrollView>
- )
- }
- ```
-
- The native approach handles dynamic safe areas (keyboard, toolbars) and allows content to scroll behind the status bar naturally.
-
- ### 8.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**
-
- ```tsx
- import { Image } from 'react-native'
-
- function Avatar({ url }: { url: string }) {
- return <Image source={{ uri: url }} style={styles.avatar} />
- }
- ```
-
- **Correct: expo-image**
-
- ```tsx
- import { Image } from 'expo-image'
-
- function Avatar({ url }: { url: string }) {
- return <Image source={{ uri: url }} style={styles.avatar} />
- }
- ```
-
- **With blurhash placeholder:**
-
- ```tsx
- <Image
- source={{ uri: url }}
- placeholder={{ blurhash: 'LGF5]+Yk^6#M@-5c,1J5@[or[Q6.' }}
- contentFit="cover"
- transition={200}
- style={styles.image}
- />
- ```
-
- **With priority and caching:**
-
- ```tsx
- <Image
- source={{ uri: url }}
- priority="high"
- cachePolicy="memory-disk"
- style={styles.hero}
- />
- ```
-
- **Key props:**
-
- - `placeholder` — Blurhash or thumbnail while loading
-
- - `contentFit` — `cover`, `contain`, `fill`, `scale-down`
-
- - `transition` — Fade-in duration (ms)
-
- - `priority` — `low`, `normal`, `high`
-
- - `cachePolicy` — `memory`, `disk`, `memory-disk`, `none`
-
- - `recyclingKey` — Unique key for list recycling
-
- For cross-platform (web + native), use `SolitoImage` from `solito/image` which uses `expo-image` under the hood.
-
- Reference: [https://docs.expo.dev/versions/latest/sdk/image/](https://docs.expo.dev/versions/latest/sdk/image/)
-
- ### 8.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**
-
- ```tsx
- function ImageGallery({ urls }: { urls: string[] }) {
- const [selected, setSelected] = useState<string | null>(null)
-
- return (
- <>
- {urls.map((url) => (
- <Pressable key={url} onPress={() => setSelected(url)}>
- <Image source={{ uri: url }} style={styles.thumbnail} />
- </Pressable>
- ))}
- <Modal visible={!!selected} onRequestClose={() => setSelected(null)}>
- <Image source={{ uri: selected! }} style={styles.fullscreen} />
- </Modal>
- </>
- )
- }
- ```
-
- **Correct: Galeria with expo-image**
-
- ```tsx
- import { Galeria } from '@nandorojo/galeria'
- import { Image } from 'expo-image'
-
- function ImageGallery({ urls }: { urls: string[] }) {
- return (
- <Galeria urls={urls}>
- {urls.map((url, index) => (
- <Galeria.Image index={index} key={url}>
- <Image source={{ uri: url }} style={styles.thumbnail} />
- </Galeria.Image>
- ))}
- </Galeria>
- )
- }
- ```
-
- **Single image:**
-
- ```tsx
- import { Galeria } from '@nandorojo/galeria'
- import { Image } from 'expo-image'
-
- function Avatar({ url }: { url: string }) {
- return (
- <Galeria urls={[url]}>
- <Galeria.Image>
- <Image source={{ uri: url }} style={styles.avatar} />
- </Galeria.Image>
- </Galeria>
- )
- }
- ```
-
- **With low-res thumbnails and high-res fullscreen:**
-
- ```tsx
- <Galeria urls={highResUrls}>
- {lowResUrls.map((url, index) => (
- <Galeria.Image index={index} key={url}>
- <Image source={{ uri: url }} style={styles.thumbnail} />
- </Galeria.Image>
- ))}
- </Galeria>
- ```
-
- **With FlashList:**
-
- ```tsx
- <Galeria urls={urls}>
- <FlashList
- data={urls}
- renderItem={({ item, index }) => (
- <Galeria.Image index={index}>
- <Image source={{ uri: item }} style={styles.thumbnail} />
- </Galeria.Image>
- )}
- numColumns={3}
- estimatedItemSize={100}
- />
- </Galeria>
- ```
-
- Works with `expo-image`, `SolitoImage`, `react-native` Image, or any image
-
- component.
-
- Reference: [https://github.com/nandorojo/galeria](https://github.com/nandorojo/galeria)
-
- ### 8.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](https://zeego.dev) for cross-platform native menus.
-
- **Incorrect: custom JS menu**
-
- ```tsx
- import { useState } from 'react'
- import { View, Pressable, Text } from 'react-native'
-
- function MyMenu() {
- const [open, setOpen] = useState(false)
-
- return (
- <View>
- <Pressable onPress={() => setOpen(!open)}>
- <Text>Open Menu</Text>
- </Pressable>
- {open && (
- <View style={{ position: 'absolute', top: 40 }}>
- <Pressable onPress={() => console.log('edit')}>
- <Text>Edit</Text>
- </Pressable>
- <Pressable onPress={() => console.log('delete')}>
- <Text>Delete</Text>
- </Pressable>
- </View>
- )}
- </View>
- )
- }
- ```
-
- **Correct: native menu with zeego**
-
- ```tsx
- import * as DropdownMenu from 'zeego/dropdown-menu'
-
- function MyMenu() {
- return (
- <DropdownMenu.Root>
- <DropdownMenu.Trigger>
- <Pressable>
- <Text>Open Menu</Text>
- </Pressable>
- </DropdownMenu.Trigger>
-
- <DropdownMenu.Content>
- <DropdownMenu.Item key='edit' onSelect={() => console.log('edit')}>
- <DropdownMenu.ItemTitle>Edit</DropdownMenu.ItemTitle>
- </DropdownMenu.Item>
-
- <DropdownMenu.Item
- key='delete'
- destructive
- onSelect={() => console.log('delete')}
- >
- <DropdownMenu.ItemTitle>Delete</DropdownMenu.ItemTitle>
- </DropdownMenu.Item>
- </DropdownMenu.Content>
- </DropdownMenu.Root>
- )
- }
- ```
-
- **Context menu: long-press**
-
- ```tsx
- import * as ContextMenu from 'zeego/context-menu'
-
- function MyContextMenu() {
- return (
- <ContextMenu.Root>
- <ContextMenu.Trigger>
- <View style={{ padding: 20 }}>
- <Text>Long press me</Text>
- </View>
- </ContextMenu.Trigger>
-
- <ContextMenu.Content>
- <ContextMenu.Item key='copy' onSelect={() => console.log('copy')}>
- <ContextMenu.ItemTitle>Copy</ContextMenu.ItemTitle>
- </ContextMenu.Item>
-
- <ContextMenu.Item key='paste' onSelect={() => console.log('paste')}>
- <ContextMenu.ItemTitle>Paste</ContextMenu.ItemTitle>
- </ContextMenu.Item>
- </ContextMenu.Content>
- </ContextMenu.Root>
- )
- }
- ```
-
- **Checkbox items:**
-
- ```tsx
- import * as DropdownMenu from 'zeego/dropdown-menu'
-
- function SettingsMenu() {
- const [notifications, setNotifications] = useState(true)
-
- return (
- <DropdownMenu.Root>
- <DropdownMenu.Trigger>
- <Pressable>
- <Text>Settings</Text>
- </Pressable>
- </DropdownMenu.Trigger>
-
- <DropdownMenu.Content>
- <DropdownMenu.CheckboxItem
- key='notifications'
- value={notifications}
- onValueChange={() => setNotifications((prev) => !prev)}
- >
- <DropdownMenu.ItemIndicator />
- <DropdownMenu.ItemTitle>Notifications</DropdownMenu.ItemTitle>
- </DropdownMenu.CheckboxItem>
- </DropdownMenu.Content>
- </DropdownMenu.Root>
- )
- }
- ```
-
- **Submenus:**
-
- ```tsx
- import * as DropdownMenu from 'zeego/dropdown-menu'
-
- function MenuWithSubmenu() {
- return (
- <DropdownMenu.Root>
- <DropdownMenu.Trigger>
- <Pressable>
- <Text>Options</Text>
- </Pressable>
- </DropdownMenu.Trigger>
-
- <DropdownMenu.Content>
- <DropdownMenu.Item key='home' onSelect={() => console.log('home')}>
- <DropdownMenu.ItemTitle>Home</DropdownMenu.ItemTitle>
- </DropdownMenu.Item>
-
- <DropdownMenu.Sub>
- <DropdownMenu.SubTrigger key='more'>
- <DropdownMenu.ItemTitle>More Options</DropdownMenu.ItemTitle>
- </DropdownMenu.SubTrigger>
-
- <DropdownMenu.SubContent>
- <DropdownMenu.Item key='settings'>
- <DropdownMenu.ItemTitle>Settings</DropdownMenu.ItemTitle>
- </DropdownMenu.Item>
-
- <DropdownMenu.Item key='help'>
- <DropdownMenu.ItemTitle>Help</DropdownMenu.ItemTitle>
- </DropdownMenu.Item>
- </DropdownMenu.SubContent>
- </DropdownMenu.Sub>
- </DropdownMenu.Content>
- </DropdownMenu.Root>
- )
- }
- ```
-
- Reference: [https://zeego.dev/components/dropdown-menu](https://zeego.dev/components/dropdown-menu)
-
- ### 8.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**
-
- ```tsx
- import BottomSheet from 'custom-js-bottom-sheet'
-
- function MyScreen() {
- const sheetRef = useRef<BottomSheet>(null)
-
- return (
- <View style={{ flex: 1 }}>
- <Button onPress={() => sheetRef.current?.expand()} title='Open' />
- <BottomSheet ref={sheetRef} snapPoints={['50%', '90%']}>
- <View>
- <Text>Sheet content</Text>
- </View>
- </BottomSheet>
- </View>
- )
- }
- ```
-
- **Correct: native Modal with formSheet**
-
- ```tsx
- import { Modal, View, Text, Button } from 'react-native'
-
- function MyScreen() {
- const [visible, setVisible] = useState(false)
-
- return (
- <View style={{ flex: 1 }}>
- <Button onPress={() => setVisible(true)} title='Open' />
- <Modal
- visible={visible}
- presentationStyle='formSheet'
- animationType='slide'
- onRequestClose={() => setVisible(false)}
- >
- <View>
- <Text>Sheet content</Text>
- </View>
- </Modal>
- </View>
- )
- }
- ```
-
- **Correct: React Navigation v7 native form sheet**
-
- ```tsx
- // In your navigator
- <Stack.Screen
- name='Details'
- component={DetailsScreen}
- options={{
- presentation: 'formSheet',
- sheetAllowedDetents: 'fitToContents',
- }}
- />
- ```
-
- Native modals provide swipe-to-dismiss, proper keyboard avoidance, and
-
- accessibility out of the box.
-
- ### 8.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**
-
- ```tsx
- import { TouchableOpacity } from 'react-native'
-
- function MyButton({ onPress }: { onPress: () => void }) {
- return (
- <TouchableOpacity onPress={onPress} activeOpacity={0.7}>
- <Text>Press me</Text>
- </TouchableOpacity>
- )
- }
- ```
-
- **Correct: Pressable**
-
- ```tsx
- import { Pressable } from 'react-native'
-
- function MyButton({ onPress }: { onPress: () => void }) {
- return (
- <Pressable onPress={onPress}>
- <Text>Press me</Text>
- </Pressable>
- )
- }
- ```
-
- **Correct: Pressable from gesture handler for lists**
-
- ```tsx
- import { Pressable } from 'react-native-gesture-handler'
-
- function ListItem({ onPress }: { onPress: () => void }) {
- return (
- <Pressable onPress={onPress}>
- <Text>Item</Text>
- </Pressable>
- )
- }
- ```
-
- Use `react-native-gesture-handler` Pressable inside scrollable lists for better
- gesture coordination, as long as you are using the ScrollView from
- `react-native-gesture-handler` as well.
-
- **For animated press states (scale, opacity changes):** Use `GestureDetector`
- with Reanimated shared values instead of Pressable's style callback. See rule
- 3.3 (Use GestureDetector for Animated Press States).
-
- ---
-
- ## 9. Design System
-
- **Impact: MEDIUM**
-
- Architecture patterns for building maintainable component
- libraries.
-
- ### 9.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**
-
- ```tsx
- import { Pressable, Text } from 'react-native'
-
- type ButtonProps = {
- children: string | React.ReactNode
- icon?: React.ReactNode
- }
-
- function Button({ children, icon }: ButtonProps) {
- return (
- <Pressable>
- {icon}
- {typeof children === 'string' ? <Text>{children}</Text> : children}
- </Pressable>
- )
- }
-
- // Usage is ambiguous
- <Button icon={<Icon />}>Save</Button>
- <Button><CustomText>Save</CustomText></Button>
- ```
-
- **Correct: compound components**
-
- ```tsx
- import { Pressable, Text } from 'react-native'
-
- function Button({ children }: { children: React.ReactNode }) {
- return <Pressable>{children}</Pressable>
- }
-
- function ButtonText({ children }: { children: React.ReactNode }) {
- return <Text>{children}</Text>
- }
-
- function ButtonIcon({ children }: { children: React.ReactNode }) {
- return <>{children}</>
- }
-
- // Usage is explicit and composable
- <Button>
- <ButtonIcon><SaveIcon /></ButtonIcon>
- <ButtonText>Save</ButtonText>
- </Button>
-
- <Button>
- <ButtonText>Cancel</ButtonText>
- </Button>
- ```
-
- ---
-
- ## 10. Monorepo
-
- **Impact: LOW**
-
- Dependency management and native module configuration in
- monorepos.
-
- ### 10.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**
-
- ```typescript
- 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**
-
- ```json
- // packages/app/package.json
- {
- "dependencies": {
- "react-native-reanimated": "3.16.1"
- }
- }
- ```
-
- Even if the shared package uses the native dependency, the app must also list it
-
- for autolinking to detect and link the native code.
-
- ### 10.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**
-
- ```json
- // packages/app/package.json
- {
- "dependencies": {
- "react-native-reanimated": "^3.0.0"
- }
- }
-
- // packages/ui/package.json
- {
- "dependencies": {
- "react-native-reanimated": "^3.5.0"
- }
- }
- ```
-
- **Correct: exact versions, single source of truth**
-
- ```json
- // package.json (root)
- {
- "pnpm": {
- "overrides": {
- "react-native-reanimated": "3.16.1"
- }
- }
- }
-
- // packages/app/package.json
- {
- "dependencies": {
- "react-native-reanimated": "3.16.1"
- }
- }
-
- // packages/ui/package.json
- {
- "dependencies": {
- "react-native-reanimated": "3.16.1"
- }
- }
- ```
-
- Use your package manager's override/resolution feature to enforce versions at
-
- the root. When adding dependencies, specify exact versions without `^` or `~`.
-
- ---
-
- ## 11. Third-Party Dependencies
-
- **Impact: LOW**
-
- Wrapping and re-exporting third-party dependencies for
- maintainability.
-
- ### 11.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**
-
- ```tsx
- import { View, Text } from 'react-native'
- import { Button } from '@ui/button'
-
- function Profile() {
- return (
- <View>
- <Text>Hello</Text>
- <Button>Save</Button>
- </View>
- )
- }
- ```
-
- **Correct: imports from design system**
-
- ```tsx
- 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.
-
- ---
-
- ## 12. JavaScript
-
- **Impact: LOW**
-
- Micro-optimizations like hoisting expensive object creation.
-
- ### 12.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**
-
- ```tsx
- function Price({ amount }: { amount: number }) {
- const formatter = new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: 'USD',
- })
- return <Text>{formatter.format(amount)}</Text>
- }
- ```
-
- **Correct: hoisted to module scope**
-
- ```tsx
- const currencyFormatter = new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency: 'USD',
- })
-
- function Price({ amount }: { amount: number }) {
- return <Text>{currencyFormatter.format(amount)}</Text>
- }
- ```
-
- **For dynamic locales, memoize:**
-
- ```tsx
- const dateFormatter = useMemo(
- () => new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }),
- [locale]
- )
- ```
-
- **Common formatters to hoist:**
-
- ```tsx
- // Module-level formatters
- const dateFormatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' })
- const timeFormatter = new Intl.DateTimeFormat('en-US', { timeStyle: 'short' })
- const percentFormatter = new Intl.NumberFormat('en-US', { style: 'percent' })
- const relativeFormatter = new Intl.RelativeTimeFormat('en-US', {
- numeric: 'auto',
- })
- ```
-
- Creating `Intl` objects is significantly more expensive than `RegExp` or plain
-
- objects—each instantiation parses locale data and builds internal lookup tables.
-
- ---
-
- ## 13. Fonts
-
- **Impact: LOW**
-
- Native font loading for improved performance.
-
- ### 13.1 Load fonts natively at build time
+ 5. [Navigation](#5-navigation) — **HIGH**
+ - 5.1 [Use Native Navigators for Navigation](#51-use-native-navigators-for-navigation)
+ 6. [React State](#6-react-state) — **MEDIUM**
+ - 6.1 [Minimize State Variables and Derive Values](#61-minimize-state-variables-and-derive-values)
+ - 6.2 [Use fallback state instead of initialState](#62-use-fallback-state-instead-of-initialstate)
+ - 6.3 [useState Dispatch updaters for State That Depends on Current Value](#63-usestate-dispatch-updaters-for-state-that-depends-on-current-value)
+ 7. [State Architecture](#7-state-architecture) — **MEDIUM**
+ - 7.1 [State Must Represent Ground Truth](#71-state-must-represent-ground-truth)
+ 8. [React Compiler](#8-react-compiler) — **MEDIUM**
+ - 8.1 [Destructure Functions Early in Render (React Compiler)](#81-destructure-functions-early-in-render-react-compiler)
+ - 8.2 [Use .get() and .set() for Reanimated Shared Values (not .value)](#82-use-get-and-set-for-reanimated-shared-values-not-value)
+ 9. [User Interface](#9-user-interface) — **MEDIUM**
+ - 9.1 [Measuring View Dimensions](#91-measuring-view-dimensions)
+ - 9.2 [Modern React Native Styling Patterns](#92-modern-react-native-styling-patterns)
+ - 9.3 [Use contentInset for Dynamic ScrollView Spacing](#93-use-contentinset-for-dynamic-scrollview-spacing)
+ - 9.4 [Use contentInsetAdjustmentBehavior for Safe Areas](#94-use-contentinsetadjustmentbehavior-for-safe-areas)
+ - 9.5 [Use expo-image for Optimized Images](#95-use-expo-image-for-optimized-images)
+ - 9.6 [Use Galeria for Image Galleries and Lightbox](#96-use-galeria-for-image-galleries-and-lightbox)
+ - 9.7 [Use Native Menus for Dropdowns and Context Menus](#97-use-native-menus-for-dropdowns-and-context-menus)
+ - 9.8 [Use Native Modals Over JS-Based Bottom Sheets](#98-use-native-modals-over-js-based-bottom-sheets)
+ - 9.9 [Use Pressable Instead of Touchable Components](#99-use-pressable-instead-of-touchable-components)
+ 10. [Design System](#10-design-system) — **MEDIUM**
+ - 10.1 [Use Compound Components Over Polymorphic Children](#101-use-compound-components-over-polymorphic-children)
+ 11. [Monorepo](#11-monorepo) — **LOW**
+ - 11.1 [Install Native Dependencies in App Directory](#111-install-native-dependencies-in-app-directory)
+ - 11.2 [Use Single Dependency Versions Across Monorepo](#112-use-single-dependency-versions-across-monorepo)
+ 12. [Third-Party Dependencies](#12-third-party-dependencies) — **LOW**
+ - 12.1 [Import from Design System Folder](#121-import-from-design-system-folder)
+ 13. [JavaScript](#13-javascript) — **LOW**
+ - 13.1 [Hoist Intl Formatter Creation](#131-hoist-intl-formatter-creation)
+ 14. [Fonts](#14-fonts) — **LOW**
+ - 14.1 [Load fonts natively at build time](#141-load-fonts-natively-at-build-time)
+
+ ---
+
+ ## 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 ""**
+
+ ```tsx
+ function Profile({ name, count }: { name: string; count: number }) {
+ return (
+ <View>
+ {name && <Text>{name}</Text>}
+ {count && <Text>{count} items</Text>}
+ </View>
+ )
+ }
+ // If name="" or count=0, renders the falsy value → crash
+ ```
+
+ **Correct: ternary with null**
+
+ ```tsx
+ function Profile({ name, count }: { name: string; count: number }) {
+ return (
+ <View>
+ {name ? <Text>{name}</Text> : null}
+ {count ? <Text>{count} items</Text> : null}
+ </View>
+ )
+ }
+ ```
+
+ **Correct: explicit boolean coercion**
+
+ ```tsx
+ function Profile({ name, count }: { name: string; count: number }) {
+ return (
+ <View>
+ {!!name && <Text>{name}</Text>}
+ {!!count && <Text>{count} items</Text>}
+ </View>
+ )
+ }
+ ```
+
+ **Best: early return**
+
+ ```tsx
+ function Profile({ name, count }: { name: string; count: number }) {
+ if (!name) return null
+
+ return (
+ <View>
+ <Text>{name}</Text>
+ {count > 0 ? <Text>{count} items</Text> : null}
+ </View>
+ )
+ }
+ ```
+
+ Early returns are clearest. When using conditionals inline, prefer ternary or
+
+ explicit boolean checks.
+
+ **Lint rule:** Enable `react/jsx-no-leaked-render` from
+
+ [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-leaked-render.md)
+
+ to catch this automatically.
+
+ ### 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**
+
+ ```tsx
+ import { View } from 'react-native'
+
+ function Greeting({ name }: { name: string }) {
+ return <View>Hello, {name}!</View>
+ }
+ // Error: Text strings must be rendered within a <Text> component.
+ ```
+
+ **Correct:**
+
+ ```tsx
+ import { View, Text } from 'react-native'
+
+ function Greeting({ name }: { name: string }) {
+ return (
+ <View>
+ <Text>Hello, {name}!</Text>
+ </View>
+ )
+ }
+ ```
+
+ ---
+
+ ## 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**
+
+ ```tsx
+ function UserList({ users }: { users: User[] }) {
+ return (
+ <LegendList
+ data={users}
+ renderItem={({ item }) => (
+ <UserRow
+ // Bad: new object on every render
+ user={{ id: item.id, name: item.name, avatar: item.avatar }}
+ />
+ )}
+ />
+ )
+ }
+ ```
+
+ **Incorrect: inline style object**
+
+ ```tsx
+ renderItem={({ item }) => (
+ <UserRow
+ name={item.name}
+ // Bad: new style object on every render
+ style={{ backgroundColor: item.isActive ? 'green' : 'gray' }}
+ />
+ )}
+ ```
+
+ **Correct: pass item directly or primitives**
+
+ ```tsx
+ function UserList({ users }: { users: User[] }) {
+ return (
+ <LegendList
+ data={users}
+ renderItem={({ item }) => (
+ // Good: pass the item directly
+ <UserRow user={item} />
+ )}
+ />
+ )
+ }
+ ```
+
+ **Correct: pass primitives, derive inside child**
+
+ ```tsx
+ renderItem={({ item }) => (
+ <UserRow
+ id={item.id}
+ name={item.name}
+ isActive={item.isActive}
+ />
+ )}
+
+ const UserRow = memo(function UserRow({ id, name, isActive }: Props) {
+ // Good: derive style inside memoized component
+ const backgroundColor = isActive ? 'green' : 'gray'
+ return <View style={[styles.row, { backgroundColor }]}>{/* ... */}</View>
+ })
+ ```
+
+ **Correct: hoist static styles in module scope**
+
+ ```tsx
+ const activeStyle = { backgroundColor: 'green' }
+ const inactiveStyle = { backgroundColor: 'gray' }
+
+ renderItem={({ item }) => (
+ <UserRow
+ name={item.name}
+ // Good: stable references
+ style={item.isActive ? activeStyle : inactiveStyle}
+ />
+ )}
+ ```
+
+ Passing primitives or stable references allows `memo()` to skip re-renders when
+
+ the actual values haven't changed.
+
+ **Note:** If you have the React Compiler enabled, it handles memoization
+
+ automatically and these manual optimizations become less critical.
+
+ ### 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**
+
+ ```typescript
+ return (
+ <LegendList
+ renderItem={({ item }) => {
+ // bad: creates a new callback on each render
+ const onPress = () => handlePress(item.id)
+ return <Item key={item.id} item={item} onPress={onPress} />
+ }}
+ />
+ )
+ ```
+
+ **Correct: a single function instance passed to each item**
+
+ ```typescript
+ const onPress = useCallback(() => handlePress(item.id), [handlePress, item.id])
+
+ return (
+ <LegendList
+ renderItem={({ item }) => (
+ <Item key={item.id} item={item} onPress={onPress} />
+ )}
+ />
+ )
+ ```
+
+ Reference: [https://example.com](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**
+
+ ```tsx
+ function ProductRow({ id }: { id: string }) {
+ // Bad: query inside list item
+ const { data: product } = useQuery(['product', id], () => fetchProduct(id))
+ // Bad: multiple context accesses
+ const theme = useContext(ThemeContext)
+ const user = useContext(UserContext)
+ const cart = useContext(CartContext)
+ // Bad: expensive computation
+ const recommendations = useMemo(
+ () => computeRecommendations(product),
+ [product]
+ )
+
+ return <View>{/* ... */}</View>
+ }
+ ```
+
+ **Correct: lightweight list item**
+
+ ```tsx
+ function ProductRow({ name, price, imageUrl }: Props) {
+ // Good: receives only primitives, minimal hooks
+ return (
+ <View>
+ <Image source={{ uri: imageUrl }} />
+ <Text>{name}</Text>
+ <Text>{price}</Text>
+ </View>
+ )
+ }
+ ```
+
+ **Move data fetching to parent:**
+
+ ```tsx
+ // Parent fetches all data once
+ function ProductList() {
+ const { data: products } = useQuery(['products'], fetchProducts)
+
+ return (
+ <LegendList
+ data={products}
+ renderItem={({ item }) => (
+ <ProductRow name={item.name} price={item.price} imageUrl={item.image} />
+ )}
+ />
+ )
+ }
+ ```
+
+ **For shared values, use Zustand selectors instead of Context:**
+
+ ```tsx
+ // Incorrect: Context causes re-render when any cart value changes
+ function ProductRow({ id, name }: Props) {
+ const { items } = useContext(CartContext)
+ const inCart = items.includes(id)
+ // ...
+ }
+
+ // Correct: Zustand selector only re-renders when this specific value changes
+ function ProductRow({ id, name }: Props) {
+ // use Set.has (created once at the root) instead of Array.includes()
+ const inCart = useCartStore((s) => s.items.has(id))
+ // ...
+ }
+ ```
+
+ **Guidelines for list items:**
+
+ - No queries or data fetching
+
+ - No expensive computations (move to parent or memoize at parent level)
+
+ - Prefer Zustand selectors over React Context
+
+ - Minimize useState/useEffect hooks
+
+ - Pass pre-computed values as props
+
+ The goal: list items should be simple rendering functions that take props and
+
+ return JSX.
+
+ ### 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**
+
+ ```tsx
+ function DomainSearch() {
+ const { keyword, setKeyword } = useKeywordZustandState()
+ const { data: tlds } = useTlds()
+
+ // Bad: creates new objects on every render, reparenting the entire list on every keystroke
+ const domains = tlds.map((tld) => ({
+ domain: `${keyword}.${tld.name}`,
+ tld: tld.name,
+ price: tld.price,
+ }))
+
+ return (
+ <>
+ <TextInput value={keyword} onChangeText={setKeyword} />
+ <LegendList
+ data={domains}
+ renderItem={({ item }) => <DomainItem item={item} keyword={keyword} />}
+ />
+ </>
+ )
+ }
+ ```
+
+ **Correct: stable references, transform inside items**
+
+ ```tsx
+ const renderItem = ({ item }) => <DomainItem tld={item} />
+
+ function DomainSearch() {
+ const { data: tlds } = useTlds()
+
+ return (
+ <LegendList
+ // good: as long as the data is stable, LegendList will not re-render the entire list
+ data={tlds}
+ renderItem={renderItem}
+ />
+ )
+ }
+
+ function DomainItem({ tld }: { tld: Tld }) {
+ // good: transform within items, and don't pass the dynamic data as a prop
+ // good: use a selector function from zustand to receive a stable string back
+ const domain = useKeywordZustandState((s) => s.keyword + '.' + tld.name)
+ return <Text>{domain}</Text>
+ }
+ ```
+
+ **Updating parent array reference:**
+
+ ```tsx
+ // good: creates a new array instance without mutating the inner objects
+ // good: parent array reference is unaffected by typing and updating "keyword"
+ const sortedTlds = tlds.toSorted((a, b) => a.name.localeCompare(b.name))
+
+ return <LegendList data={sortedTlds} renderItem={renderItem} />
+ ```
+
+ 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**
+