React Native Performance Agent Rules
Core Rendering
- Never use `{value && <Component />}` when value could be an empty string or 0. These are falsy but renderable — React Native will try to render them as bare text, causing a hard crash. Use a ternary with null, explicit boolean coercion with `!!`, or an early return.
- All strings must be wrapped in `<Text>`. React Native crashes if a string is a direct child of `<View>`, including template literals and string concatenation.
- Enable `react/jsx-no-leaked-render` from `eslint-plugin-react` to catch `&&` leaks automatically.
List Performance
- Use a virtualizer like LegendList or FlashList for every scrollable list, even short ones. They render only visible items, reducing memory and mount time. ScrollView renders all children upfront.
- Never `.map()` or `.filter()` data before passing it to a list's `data` prop. Each call creates new references and causes a full re-render on every parent update. Keep the raw data stable and transform inside list items.
- Do not create inline objects inside `renderItem`. Inline objects break `memo()` because they are new references every render. Pass the whole item directly or use individual primitives.
- Define callbacks at the list root with `useCallback` and pass them down. Do not create arrow functions inside `renderItem`.
- Pass primitive props (strings, numbers, booleans) to list item components so `memo()` shallow comparison works correctly.
- Keep list items lightweight. No data fetching, no `useQuery`, no expensive computations, and minimal React Context reads. Use store selectors instead of Context to avoid re-renders on unrelated state changes.
- Use a `type` field on each item and pass `getItemType={(item) => item.type}` to the list. This creates separate recycling pools so a header cell never gets recycled into an image cell.
- Request images at 2x display size from your CDN or image service. Full-resolution images in list cells cause scroll jank and excess memory usage.
Animation
- Only animate `transform` (scale, translate, rotate) and `opacity`. Animating `width`, `height`, `top`, `left`, `margin`, or `padding` triggers a layout pass every frame.
- Use `useDerivedValue` when computing a shared value from another shared value. It is declarative and auto-tracks dependencies. Use `useAnimatedReaction` only for side effects like haptics, logging, or `runOnJS` calls.
- Use `GestureDetector` with `Gesture.Tap()` for animated press states instead of Pressable's `onPressIn`/`onPressOut`. Gesture callbacks run on the UI thread as worklets with no JS bridge round-trip.
- Store the press state (0 or 1) in a shared value and derive the visual (scale, opacity) via `interpolate`.
Scroll Performance
- Never store scroll position in `useState`. Scroll events fire at 60–120 fps and each `setState` triggers a re-render. Use `useSharedValue` with `useAnimatedScrollHandler` for scroll-driven animations. Use `useRef` for tracking position without rendering.
Navigation
- Always use native navigators. Use `@react-navigation/native-stack` or expo-router's default `<Stack>` for stacks. Use native tab implementations for tab bars. Avoid JS-based navigation implementations which are slower.
- Use native header options (`title`, `headerLargeTitleEnabled`, `headerSearchBarOptions`) instead of a custom `header` component. Native headers handle platform-specific behavior automatically.
React State
- Compute derived values during render. Do not store totals, counts, or computed strings in `useState` and sync them with `useEffect`.
- Initialize controlled state as `undefined` and fall back to the server or parent value with `??`. State represents user intent — `undefined` means the user has not interacted yet.
- When new state depends on current state, use the functional updater form `setState(prev => ...)`. Reading state directly in callbacks creates stale closures.
State Architecture
- State variables should represent what is happening (`pressed`, `isOpen`, `progress`), not the visual output (`scale`, `opacity`, `height`). Derive visual values from state via computation or `interpolate`. This lets one state source drive multiple animations and makes debugging simpler.
User Interface
- Use `expo-image` instead of React Native's `Image`. It provides memory-efficient caching, blurhash placeholders, progressive loading, `contentFit`, `priority`, and `cachePolicy`.
- Use native `<Modal presentationStyle="formSheet">` or native form sheets from your navigation library instead of JS-based bottom sheet libraries.
- Use `contentInsetAdjustmentBehavior="automatic"` on the root `<ScrollView>` instead of `<SafeAreaView>` or manual insets.
- Use `contentInset={{ bottom }}` instead of `contentContainerStyle={{ paddingBottom }}` for spacing that changes dynamically. Inset changes do not trigger layout recalculation.
- Use `<Pressable>` instead of deprecated `TouchableOpacity` or `TouchableHighlight`.
Styling
- Add `borderCurve: 'continuous'` to any style that uses `borderRadius` for smooth platform-native corners.
- Apply `gap` on the parent container for sibling spacing instead of `marginBottom` or `marginRight` on each child.
- Use `boxShadow` string syntax instead of legacy shadow objects or `elevation`.
- Limit font sizes to one or two per screen. Use `fontWeight` and grayscale colors for hierarchy instead of varying `fontSize`.
- Use `useLayoutEffect` for synchronous initial measurement and `onLayout` for subsequent changes.
Monorepo
- Install native dependencies inside the app workspace directory, not at the monorepo root. Native packages link to `node_modules` relative to the app directory.
- Keep a single version of every package across the monorepo. Duplicate versions cause multiple React instances, hook errors, and hard-to-diagnose runtime crashes. Enforce with pnpm `overrides` or Yarn `resolutions`.
JavaScript
- Create `Intl.DateTimeFormat` and `Intl.NumberFormat` instances once at module scope. Never construct them inside a render function or inside list `renderItem`.
Fonts
- Load fonts natively at build time via `app.json` assets and `expo-font`. Avoid runtime JS font loading, which causes flash of unstyled text and layout shifts on first render.