diff --git a/.github/skills/react-native-skills/AGENTS.md b/.github/skills/react-native-skills/AGENTS.md new file mode 100644 index 0000000..c3f43cf --- /dev/null +++ b/.github/skills/react-native-skills/AGENTS.md @@ -0,0 +1,1051 @@ +# React Native & Expo Skills + +**Version 2.0.0** +BitSleuth +January 2026 + +> **Note:** +> This document is 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 + +Performance and best practices guide for React Native and Expo mobile applications targeting iOS and Android. Contains 25+ rules across 11 categories covering core rendering, list performance, animations with Reanimated, navigation with Expo Router, styling with NativeWind, and platform-specific optimizations. Each rule includes detailed explanations, incorrect vs. correct code examples, and impact assessments. + +--- + +## 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 [Use a List Virtualizer for Any List](#21-use-a-list-virtualizer-for-any-list) + - 2.2 [Optimize List Performance with Stable Object References](#22-optimize-list-performance-with-stable-object-references) + - 2.3 [Avoid Inline Objects in renderItem](#23-avoid-inline-objects-in-renderitem) + - 2.4 [Hoist Callbacks to the Root of Lists](#24-hoist-callbacks-to-the-root-of-lists) + - 2.5 [Pass Primitives to List Items for Memoization](#25-pass-primitives-to-list-items-for-memoization) + - 2.6 [Keep List Items Lightweight](#26-keep-list-items-lightweight) + - 2.7 [Use Compressed Images in Lists](#27-use-compressed-images-in-lists) + - 2.8 [Use Item Types for Heterogeneous Lists](#28-use-item-types-for-heterogeneous-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. [Navigation](#5-navigation) — **HIGH** + - 5.1 [Use Expo Router for File-Based Navigation](#51-use-expo-router-for-file-based-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 [Use Dispatch Updaters for State That Depends on Current Value](#63-use-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](#81-destructure-functions-early-in-render) + - 8.2 [Use .get() and .set() for Reanimated Shared Values](#82-use-get-and-set-for-reanimated-shared-values) +9. [User Interface](#9-user-interface) — **MEDIUM** + - 9.1 [Use expo-image for Optimized Images](#91-use-expo-image-for-optimized-images) + - 9.2 [Styling with NativeWind and Modern Patterns](#92-styling-with-nativewind-and-modern-patterns) + - 9.3 [Use Pressable Instead of Touchable Components](#93-use-pressable-instead-of-touchable-components) + - 9.4 [Use contentInsetAdjustmentBehavior for Safe Areas](#94-use-contentinsetadjustmentbehavior-for-safe-areas) + - 9.5 [Use contentInset for Dynamic ScrollView Spacing](#95-use-contentinset-for-dynamic-scrollview-spacing) + - 9.6 [Use Native Menus for Dropdowns and Context Menus](#96-use-native-menus-for-dropdowns-and-context-menus) + - 9.7 [Use Native Modals Over JS-Based Bottom Sheets](#97-use-native-modals-over-js-based-bottom-sheets) + - 9.8 [Measuring View Dimensions](#98-measuring-view-dimensions) +10. [JavaScript](#10-javascript) — **LOW** + - 10.1 [Hoist Intl Formatter Creation](#101-hoist-intl-formatter-creation) +11. [Fonts](#11-fonts) — **LOW** + - 11.1 [Load Fonts Natively at Build Time](#111-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 && }` 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 `` 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 ( + + {name && {name}} + {count && {count} items} + + ) +} +// If name="" or count=0, renders the falsy value → crash +``` + +**Correct (ternary with null):** + +```tsx +function Profile({ name, count }: { name: string; count: number }) { + return ( + + {name ? {name} : null} + {count ? {count} items : null} + + ) +} +``` + +**Correct (explicit boolean coercion):** + +```tsx +function Profile({ name, count }: { name: string; count: number }) { + return ( + + {!!name && {name}} + {!!count && {count} items} + + ) +} +``` + +**Best (early return):** + +```tsx +function Profile({ name, count }: { name: string; count: number }) { + if (!name) return null + + return ( + + {name} + {count > 0 ? {count} items : null} + + ) +} +``` + +**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). + +### 1.2 Wrap Strings in Text Components + +**Impact: CRITICAL (prevents runtime crash)** + +Strings must be rendered inside ``. React Native crashes if a string is a direct child of ``. + +**Incorrect (crashes):** + +```tsx +import { View } from 'react-native' + +function Greeting({ name }: { name: string }) { + return Hello, {name}! +} +// Error: Text strings must be rendered within a component. +``` + +**Correct:** + +```tsx +import { View, Text } from 'react-native' + +function Greeting({ name }: { name: string }) { + return ( + + Hello, {name}! + + ) +} +``` + +--- + +## 2. List Performance + +**Impact: HIGH** + +Optimizing virtualized lists (FlatList, FlashList) for smooth scrolling and fast updates. + +### 2.1 Use a List Virtualizer for Any List + +**Impact: HIGH (reduced memory, faster mounts)** + +Use a list virtualizer like FlashList instead of ScrollView with mapped children—even for short lists. Virtualizers only render visible items, reducing memory usage and mount time. + +**Incorrect (ScrollView renders all items at once):** + +```tsx +function Feed({ items }: { items: Item[] }) { + return ( + + {items.map((item) => ( + + ))} + + ) +} +// 50 items = 50 components mounted, even if only 10 visible +``` + +**Correct (virtualizer renders only visible items):** + +```tsx +import { FlashList } from '@shopify/flash-list' + +function Feed({ items }: { items: Item[] }) { + return ( + } + keyExtractor={(item) => item.id} + estimatedItemSize={80} + /> + ) +} +// Only ~10-15 visible items mounted at a time +``` + +### 2.2 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. + +**Incorrect (creates new object references on every keystroke):** + +```tsx +function DomainSearch() { + const { keyword, setKeyword } = useKeywordState() + const { data: tlds } = useTlds() + + // Bad: creates new objects on every render + const domains = tlds.map((tld) => ({ + domain: `${keyword}.${tld.name}`, + tld: tld.name, + })) + + return ( + <> + + } /> + + ) +} +``` + +**Correct (stable references, transform inside items):** + +```tsx +function DomainSearch() { + const { data: tlds } = useTlds() + + return ( + } + /> + ) +} + +function DomainItem({ tld }: { tld: Tld }) { + // Transform within items using Zustand selector + const keyword = useSearchStore((s) => s.keyword) + const domain = `${keyword}.${tld.name}` + return {domain} +} +``` + +### 2.3 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. + +**Incorrect (inline object breaks memoization):** + +```tsx +renderItem={({ item }) => ( + +)} +``` + +**Correct (pass item directly or primitives):** + +```tsx +renderItem={({ item }) => ( + +)} + +const UserRow = memo(function UserRow({ id, name, isActive }: Props) { + const backgroundColor = isActive ? 'green' : 'gray' + return {/* ... */} +}) +``` + +### 2.4 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. + +**Incorrect (creates a new callback on each render):** + +```tsx +renderItem={({ item }) => { + const onPress = () => handlePress(item.id) // Bad: new callback each render + return +}} +``` + +**Correct (single function instance):** + +```tsx +const onPress = useCallback((id: string) => handlePress(id), [handlePress]) + +renderItem={({ item }) => ( + +)} +``` + +### 2.5 Pass Primitives to List Items for Memoization + +**Impact: HIGH (enables effective memo() comparison)** + +Pass only primitive values (strings, numbers, booleans) as props to list item components. Primitives enable shallow comparison in `memo()` to work correctly. + +**Incorrect (object prop requires deep comparison):** + +```tsx +const UserRow = memo(function UserRow({ user }: { user: User }) { + return {user.name} +}) + +renderItem={({ item }) => } +``` + +**Correct (primitive props):** + +```tsx +const UserRow = memo(function UserRow({ id, name, email }: Props) { + return {name} +}) + +renderItem={({ item }) => ( + +)} +``` + +### 2.6 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. + +**Incorrect (heavy list item):** + +```tsx +function ProductRow({ id }: { id: string }) { + const { data: product } = useQuery(['product', id], () => fetchProduct(id)) + const theme = useContext(ThemeContext) + const cart = useContext(CartContext) + return {/* ... */} +} +``` + +**Correct (lightweight list item):** + +```tsx +function ProductRow({ name, price, imageUrl }: Props) { + return ( + + + {name} + {price} + + ) +} +``` + +**Guidelines:** +- No queries or data fetching inside items +- Prefer Zustand selectors over React Context +- Minimize useState/useEffect hooks +- Pass pre-computed values as props + +### 2.7 Use Compressed Images in Lists + +**Impact: HIGH (faster load times, less memory)** + +Always load compressed, appropriately-sized images in lists. Request images at 2x the display size for retina screens. + +**Incorrect:** + +```tsx + +// 4000x3000 image loaded for a 100x100 thumbnail +``` + +**Correct:** + +```tsx +const thumbnailUrl = `${product.imageUrl}?w=200&h=200&fit=cover` + +``` + +### 2.8 Use Item Types for Heterogeneous Lists + +**Impact: HIGH (efficient recycling, less layout thrashing)** + +When a list has different item layouts, use a `type` field and provide `getItemType` to the list. + +**Correct (typed items with separate components):** + +```tsx +type FeedItem = HeaderItem | MessageItem | ImageItem + +function Feed({ items }: { items: FeedItem[] }) { + return ( + item.id} + getItemType={(item) => item.type} + renderItem={({ item }) => { + switch (item.type) { + case 'header': return + case 'message': return + case 'image': return + } + }} + /> + ) +} +``` + +--- + +## 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`. Use `transform` and `opacity` which run on the GPU. + +**Incorrect (animates height, triggers layout every frame):** + +```tsx +const animatedStyle = useAnimatedStyle(() => ({ + height: withTiming(expanded ? 200 : 0), +})) +``` + +**Correct (animates scaleY, GPU-accelerated):** + +```tsx +const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scaleY: withTiming(expanded ? 1 : 0) }], + opacity: withTiming(expanded ? 1 : 0), +})) +``` + +### 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`. + +**Incorrect:** + +```tsx +const progress = useSharedValue(0) +const opacity = useSharedValue(1) + +useAnimatedReaction( + () => progress.value, + (current) => { opacity.value = 1 - current } +) +``` + +**Correct:** + +```tsx +const progress = useSharedValue(0) +const opacity = useDerivedValue(() => 1 - progress.get()) +``` + +### 3.3 Use GestureDetector for Animated Press States + +**Impact: MEDIUM (UI thread animations, smoother press feedback)** + +For animated press states, use `GestureDetector` with `Gesture.Tap()` and shared values instead of Pressable's callbacks. + +**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 }) { + const pressed = useSharedValue(0) + + const tap = Gesture.Tap() + .onBegin(() => { pressed.set(withTiming(1)) }) + .onFinalize(() => { pressed.set(withTiming(0)) }) + .onEnd(() => { runOnJS(onPress)() }) + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: interpolate(pressed.get(), [0, 1], [1, 0.95]) }], + })) + + return ( + + + Press me + + + ) +} +``` + +--- + +## 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`. Use a Reanimated shared value for animations or a ref for non-reactive tracking. + +**Incorrect:** + +```tsx +const [scrollY, setScrollY] = useState(0) + +const onScroll = (e) => { + setScrollY(e.nativeEvent.contentOffset.y) // re-renders on every frame +} +``` + +**Correct (Reanimated for animations):** + +```tsx +const scrollY = useSharedValue(0) + +const onScroll = useAnimatedScrollHandler({ + onScroll: (e) => { + scrollY.value = e.contentOffset.y // runs on UI thread, no re-render + }, +}) + +return +``` + +--- + +## 5. Navigation + +**Impact: HIGH** + +Using Expo Router and native navigators for file-based routing on iOS and Android. + +### 5.1 Use Expo Router for File-Based Navigation + +**Impact: HIGH (native performance, platform-appropriate UI)** + +Use Expo Router for file-based navigation. It uses native navigators under the hood. + +**Stack Navigation:** + +```tsx +// app/_layout.tsx +import { Stack } from 'expo-router' + +export default function RootLayout() { + return ( + + + + + ) +} +``` + +**Tab Navigation:** + +```tsx +// app/(tabs)/_layout.tsx +import { Tabs } from 'expo-router' + +export default function TabLayout() { + return ( + + }} /> + }} /> + }} /> + + ) +} +``` + +**Programmatic Navigation:** + +```tsx +import { router } from 'expo-router' + +router.push('/transaction-details') +router.push({ pathname: '/transaction-details', params: { txid: '...' } }) +router.back() +router.replace('/home') +``` + +--- + +## 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)** + +If a value can be computed from existing state or props, derive it during render instead of storing it in state. + +**Incorrect:** + +```tsx +const [total, setTotal] = useState(0) +const [itemCount, setItemCount] = useState(0) + +useEffect(() => { + setTotal(items.reduce((sum, item) => sum + item.price, 0)) + setItemCount(items.length) +}, [items]) +``` + +**Correct:** + +```tsx +const total = items.reduce((sum, item) => sum + item.price, 0) +const itemCount = items.length +``` + +### 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. + +**Correct:** + +```tsx +function Toggle({ fallbackEnabled }: Props) { + const [_enabled, setEnabled] = useState(undefined) + const enabled = _enabled ?? fallbackEnabled + + return +} +``` + +### 6.3 Use Dispatch Updaters for State That Depends on Current Value + +**Impact: MEDIUM (avoids stale closures)** + +When the next state depends on the current state, use a dispatch updater. + +**Incorrect:** + +```tsx +const onTap = () => { + setCount(count + 1) // count may be stale +} +``` + +**Correct:** + +```tsx +const onTap = () => { + setCount((prev) => prev + 1) +} +``` + +--- + +## 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 should represent the actual state (e.g., `pressed`, `isOpen`), not derived visual values (e.g., `scale`, `opacity`). + +**Incorrect:** + +```tsx +const scale = useSharedValue(1) + +const tap = Gesture.Tap() + .onBegin(() => { scale.set(withTiming(0.95)) }) + .onFinalize(() => { scale.set(withTiming(1)) }) +``` + +**Correct:** + +```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]) }], +})) +``` + +--- + +## 8. React Compiler + +**Impact: MEDIUM** + +Compatibility patterns for React Compiler with React Native and Reanimated. + +### 8.1 Destructure Functions Early in Render + +**Impact: HIGH (stable references, fewer re-renders)** + +Destructure functions from hooks at the top of render scope. + +**Incorrect:** + +```tsx +function SaveButton(props) { + const router = useRouter() + + const handlePress = () => { + props.onSave() + router.push('/success') // unstable reference + } +} +``` + +**Correct:** + +```tsx +function SaveButton({ onSave }) { + const { push } = useRouter() + + const handlePress = () => { + onSave() + push('/success') // stable reference + } +} +``` + +### 8.2 Use .get() and .set() for Reanimated Shared Values + +**Impact: LOW (required for React Compiler compatibility)** + +With React Compiler enabled, use `.get()` and `.set()` instead of `.value`. + +**Incorrect:** + +```tsx +count.value = count.value + 1 +``` + +**Correct:** + +```tsx +count.set(count.get() + 1) +``` + +--- + +## 9. User Interface + +**Impact: MEDIUM** + +Native UI patterns for images, menus, modals, styling with NativeWind, and platform-consistent interfaces. + +### 9.1 Use expo-image for Optimized Images + +**Impact: HIGH (memory efficiency, caching, blurhash placeholders)** + +Use `expo-image` instead of React Native's `Image`. + +**Correct:** + +```tsx +import { Image } from 'expo-image' + + +``` + +### 9.2 Styling with NativeWind and Modern Patterns + +**Impact: MEDIUM (consistent design, cleaner layouts)** + +This project uses NativeWind (Tailwind CSS for React Native). + +**Basic usage:** + +```tsx + + {title} + +``` + +**Use gap for spacing:** + +```tsx + + Title + Subtitle + +``` + +**Platform-specific styles:** + +```tsx + +``` + +**Dark mode:** + +```tsx + + Content + +``` + +### 9.3 Use Pressable Instead of Touchable Components + +**Impact: LOW (modern API, more flexible)** + +Never use `TouchableOpacity` or `TouchableHighlight`. Use `Pressable` instead. + +**Correct:** + +```tsx +import { Pressable } from 'react-native' + + + Press me + +``` + +### 9.4 Use contentInsetAdjustmentBehavior for Safe Areas + +**Impact: MEDIUM (native safe area handling)** + +Use `contentInsetAdjustmentBehavior="automatic"` on ScrollView instead of SafeAreaView. + +**Correct:** + +```tsx + + + Content + + +``` + +### 9.5 Use contentInset for Dynamic ScrollView Spacing + +**Impact: LOW (smoother updates, no layout recalculation)** + +Use `contentInset` instead of padding for dynamic spacing. + +**Correct:** + +```tsx + + {children} + +``` + +### 9.6 Use Native Menus for Dropdowns and Context Menus + +**Impact: HIGH (native accessibility, platform-consistent UX)** + +Use native platform menus instead of custom JS implementations. + +**Correct (with zeego):** + +```tsx +import * as DropdownMenu from 'zeego/dropdown-menu' + + + + Open Menu + + + console.log('edit')}> + Edit + + + +``` + +### 9.7 Use Native Modals Over JS-Based Bottom Sheets + +**Impact: HIGH (native performance, gestures, accessibility)** + +Use native `` with `presentationStyle="formSheet"` instead of JS-based bottom sheets. + +**Correct:** + +```tsx + setVisible(false)} +> + Sheet content + +``` + +### 9.8 Measuring View Dimensions + +**Impact: MEDIUM (synchronous measurement)** + +Use both `useLayoutEffect` and `onLayout` for measuring views. + +**Correct:** + +```tsx +const ref = useRef(null) +const [size, setSize] = useState(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) => { + if (prev?.width === width && prev?.height === height) return prev + return { width, height } + }) +} + +return {children} +``` + +--- + +## 10. JavaScript + +**Impact: LOW** + +Micro-optimizations like hoisting expensive object creation. + +### 10.1 Hoist Intl Formatter Creation + +**Impact: LOW-MEDIUM (avoids expensive object recreation)** + +Don't create `Intl.DateTimeFormat` or `Intl.NumberFormat` inside render. Hoist to module scope. + +**Incorrect:** + +```tsx +function Price({ amount }: { amount: number }) { + const formatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }) + return {formatter.format(amount)} +} +``` + +**Correct:** + +```tsx +const currencyFormatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }) + +function Price({ amount }: { amount: number }) { + return {currencyFormatter.format(amount)} +} +``` + +--- + +## 11. Fonts + +**Impact: LOW** + +Native font loading for improved performance. + +### 11.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. + +**Correct (app.json):** + +```json +{ + "expo": { + "plugins": [ + ["expo-font", { "fonts": ["./assets/fonts/Geist-Bold.otf"] }] + ] + } +} +``` + +```tsx +// No loading state needed—font is already available +Hello +``` + +After adding fonts, run `npx expo prebuild` and rebuild the native app. + +--- + +## References + +1. [React Documentation](https://react.dev) +2. [React Native Documentation](https://reactnative.dev) +3. [Expo Documentation](https://docs.expo.dev) +4. [React Native Reanimated](https://docs.swmansion.com/react-native-reanimated) +5. [React Native Gesture Handler](https://docs.swmansion.com/react-native-gesture-handler) +6. [NativeWind Documentation](https://www.nativewind.dev) diff --git a/.github/skills/react-native-skills/README.md b/.github/skills/react-native-skills/README.md new file mode 100644 index 0000000..83121c1 --- /dev/null +++ b/.github/skills/react-native-skills/README.md @@ -0,0 +1,132 @@ +# React Native & Expo Skills + +Best practices for React Native and Expo mobile applications targeting iOS and +Android. Optimized for AI agents and LLMs working on this codebase. + +## Tech Stack + +- React Native 0.81+ with New Architecture +- Expo SDK 54+ +- Expo Router 5.1 (file-based navigation) +- NativeWind 4.1 (Tailwind CSS) +- React Native Reanimated 4.1 +- TypeScript 5.9 + +## Structure + +- `rules/` - Individual rule files (one per rule) + - `_sections.md` - Section metadata (titles, impacts, descriptions) + - `_template.md` - Template for creating new rules + - `{prefix}-{description}.md` - Individual rule files +- `metadata.json` - Document metadata (version, organization, abstract) +- `SKILL.md` - Skill overview and quick reference +- **`AGENTS.md`** - Compiled output for agents (generated) + +## Rules by Category + +### Core Rendering (CRITICAL) + +- `rendering-text-in-text-component.md` - Wrap strings in Text components +- `rendering-no-falsy-and.md` - Avoid falsy && operator in JSX + +### List Performance (HIGH) + +- `list-performance-virtualize.md` - Use FlashList for lists +- `list-performance-function-references.md` - Keep stable object references +- `list-performance-callbacks.md` - Hoist callbacks to list root +- `list-performance-inline-objects.md` - Avoid inline objects in renderItem +- `list-performance-item-memo.md` - Pass primitives for memoization +- `list-performance-item-expensive.md` - Keep list items lightweight +- `list-performance-images.md` - Use compressed images in lists +- `list-performance-item-types.md` - Use item types for heterogeneous lists + +### Animation (HIGH) + +- `animation-gpu-properties.md` - Animate transform/opacity only +- `animation-gesture-detector-press.md` - Use GestureDetector for press +- `animation-derived-value.md` - Prefer useDerivedValue + +### Scroll Performance (HIGH) + +- `scroll-position-no-state.md` - Never track scroll in useState + +### Navigation (HIGH) + +- `navigation-native-navigators.md` - Use Expo Router + +### React State (MEDIUM) + +- `react-state-dispatcher.md` - Use functional setState updates +- `react-state-fallback.md` - State represents user intent only +- `react-state-minimize.md` - Minimize state, derive values + +### State Architecture (MEDIUM) + +- `state-ground-truth.md` - State must represent ground truth + +### React Compiler (MEDIUM) + +- `react-compiler-destructure-functions.md` - Destructure functions early +- `react-compiler-reanimated-shared-values.md` - Use .get()/.set() + +### User Interface (MEDIUM) + +- `ui-expo-image.md` - Use expo-image for images +- `ui-menus.md` - Native dropdown and context menus +- `ui-native-modals.md` - Use native Modal with formSheet +- `ui-pressable.md` - Use Pressable over TouchableOpacity +- `ui-measure-views.md` - Measuring view dimensions +- `ui-safe-area-scroll.md` - Use contentInsetAdjustmentBehavior +- `ui-scrollview-content-inset.md` - Use contentInset for spacing +- `ui-styling.md` - NativeWind styling patterns + +### JavaScript (LOW) + +- `js-hoist-intl.md` - Hoist Intl formatter creation + +### Fonts (LOW) + +- `fonts-config-plugin.md` - Load fonts natively at build time + +## Creating a New Rule + +1. Copy `rules/_template.md` to `rules/{prefix}-{description}.md` +2. Choose the appropriate prefix from `_sections.md` +3. Fill in the frontmatter and content +4. Include clear incorrect/correct examples + +## Rule File Structure + +```markdown +--- +title: Rule Title Here +impact: MEDIUM +impactDescription: Optional description +tags: tag1, tag2, tag3 +--- + +## Rule Title Here + +Brief explanation of the rule and why it matters. + +**Incorrect (description):** + +\`\`\`tsx +// Bad code example +\`\`\` + +**Correct (description):** + +\`\`\`tsx +// Good code example +\`\`\` + +Reference: [Link](https://example.com) +``` + +## Impact Levels + +- `CRITICAL` - Causes crashes or broken UI +- `HIGH` - Significant performance improvements +- `MEDIUM` - Moderate improvements +- `LOW` - Incremental improvements diff --git a/.github/skills/react-native-skills/SKILL.md b/.github/skills/react-native-skills/SKILL.md new file mode 100644 index 0000000..5043c19 --- /dev/null +++ b/.github/skills/react-native-skills/SKILL.md @@ -0,0 +1,142 @@ +--- +name: react-native-expo-skills +description: + React Native and Expo best practices for building performant iOS and Android + mobile apps. Use when building components, optimizing list performance, + implementing animations with Reanimated, or styling with NativeWind. Triggers + on tasks involving React Native, Expo, Expo Router navigation, or mobile + performance optimization. +license: MIT +metadata: + author: BitSleuth + version: '2.0.0' +--- + +# React Native & Expo Skills + +Best practices for React Native and Expo mobile applications targeting iOS and +Android. Contains rules covering performance, animations, navigation, styling, +and platform-specific optimizations. + +## Tech Stack + +- React Native 0.81+ +- Expo SDK 54+ +- Expo Router (file-based navigation) +- NativeWind (Tailwind CSS) +- React Native Reanimated +- TypeScript + +## When to Apply + +Reference these guidelines when: + +- Building React Native or Expo apps for iOS/Android +- Optimizing list and scroll performance +- Implementing animations with Reanimated +- Configuring navigation with Expo Router +- Styling components with NativeWind +- Working with images using expo-image + +## Rule Categories by Priority + +| Priority | Category | Impact | Prefix | +| -------- | ---------------- | -------- | ------------------- | +| 1 | Core Rendering | CRITICAL | `rendering-` | +| 2 | List Performance | HIGH | `list-performance-` | +| 3 | Animation | HIGH | `animation-` | +| 4 | Scroll | HIGH | `scroll-` | +| 5 | Navigation | HIGH | `navigation-` | +| 6 | React State | MEDIUM | `react-state-` | +| 7 | State Arch | MEDIUM | `state-` | +| 8 | React Compiler | MEDIUM | `react-compiler-` | +| 9 | User Interface | MEDIUM | `ui-` | +| 10 | JavaScript | LOW | `js-` | +| 11 | Fonts | LOW | `fonts-` | + +## Quick Reference + +### 1. Core Rendering (CRITICAL) + +- `rendering-text-in-text-component` - Wrap strings in Text components +- `rendering-no-falsy-and` - Avoid falsy && for conditional rendering + +### 2. List Performance (HIGH) + +- `list-performance-virtualize` - Use FlashList for large lists +- `list-performance-item-memo` - Pass primitives for memoization +- `list-performance-callbacks` - Hoist callbacks to list root +- `list-performance-inline-objects` - Avoid inline style objects +- `list-performance-function-references` - Keep stable object references +- `list-performance-images` - Use compressed images in lists +- `list-performance-item-expensive` - Keep list items lightweight +- `list-performance-item-types` - Use item types for heterogeneous lists + +### 3. Animation (HIGH) + +- `animation-gpu-properties` - Animate transform and opacity only +- `animation-derived-value` - Use useDerivedValue for computed animations +- `animation-gesture-detector-press` - Use Gesture.Tap for press animations + +### 4. Scroll Performance (HIGH) + +- `scroll-position-no-state` - Never track scroll position in useState + +### 5. Navigation (HIGH) + +- `navigation-native-navigators` - Use Expo Router for file-based navigation + +### 6. React State (MEDIUM) + +- `react-state-minimize` - Minimize state variables, derive values +- `react-state-dispatcher` - Use functional setState updates +- `react-state-fallback` - Use fallback state instead of initialState + +### 7. State Architecture (MEDIUM) + +- `state-ground-truth` - State must represent ground truth + +### 8. React Compiler (MEDIUM) + +- `react-compiler-destructure-functions` - Destructure functions early +- `react-compiler-reanimated-shared-values` - Use .get()/.set() for shared values + +### 9. User Interface (MEDIUM) + +- `ui-expo-image` - Use expo-image for all images +- `ui-pressable` - Use Pressable over TouchableOpacity +- `ui-safe-area-scroll` - Use contentInsetAdjustmentBehavior +- `ui-scrollview-content-inset` - Use contentInset for dynamic spacing +- `ui-menus` - Use native context menus +- `ui-native-modals` - Use native modals when possible +- `ui-measure-views` - Use onLayout for measuring views +- `ui-styling` - Use NativeWind for styling + +### 10. JavaScript (LOW) + +- `js-hoist-intl` - Hoist Intl object creation + +### 11. Fonts (LOW) + +- `fonts-config-plugin` - Use config plugins for custom fonts + +## How to Use + +Read individual rule files for detailed explanations and code examples: + +``` +rules/list-performance-virtualize.md +rules/animation-gpu-properties.md +rules/ui-styling.md +``` + +Each rule file contains: + +- Brief explanation of why it matters +- Incorrect code example with explanation +- Correct code example with explanation +- Additional context and references + +## Full Compiled Document + +For the complete guide with all rules expanded: `AGENTS.md` diff --git a/.github/skills/react-native-skills/metadata.json b/.github/skills/react-native-skills/metadata.json new file mode 100644 index 0000000..37ab176 --- /dev/null +++ b/.github/skills/react-native-skills/metadata.json @@ -0,0 +1,14 @@ +{ + "version": "2.0.0", + "organization": "BitSleuth", + "date": "January 2026", + "abstract": "Performance and best practices guide for React Native and Expo mobile applications targeting iOS and Android. Contains 25+ rules across 11 categories covering core rendering, list performance, animations with Reanimated, navigation with Expo Router, styling with NativeWind, and platform-specific optimizations. Each rule includes detailed explanations, incorrect vs. correct code examples, and impact assessments.", + "references": [ + "https://react.dev", + "https://reactnative.dev", + "https://docs.expo.dev", + "https://docs.swmansion.com/react-native-reanimated", + "https://docs.swmansion.com/react-native-gesture-handler", + "https://www.nativewind.dev" + ] +} diff --git a/.github/skills/react-native-skills/rules/_sections.md b/.github/skills/react-native-skills/rules/_sections.md new file mode 100644 index 0000000..8fba312 --- /dev/null +++ b/.github/skills/react-native-skills/rules/_sections.md @@ -0,0 +1,68 @@ +# Sections + +This file defines all sections, their ordering, impact levels, and descriptions. +The section ID (in parentheses) is the filename prefix used to group rules. + +--- + +## 1. Core Rendering (rendering) + +**Impact:** CRITICAL +**Description:** Fundamental React Native rendering rules. Violations cause +runtime crashes or broken UI. + +## 2. List Performance (list-performance) + +**Impact:** HIGH +**Description:** Optimizing virtualized lists (FlatList, FlashList) for smooth +scrolling and fast updates. + +## 3. Animation (animation) + +**Impact:** HIGH +**Description:** GPU-accelerated animations, Reanimated patterns, and avoiding +render thrashing during gestures. + +## 4. Scroll Performance (scroll) + +**Impact:** HIGH +**Description:** Tracking scroll position without causing render thrashing. + +## 5. Navigation (navigation) + +**Impact:** HIGH +**Description:** Using Expo Router and native navigators for file-based routing +on iOS and Android. + +## 6. React State (react-state) + +**Impact:** MEDIUM +**Description:** Patterns for managing React state to avoid stale closures and +unnecessary re-renders. + +## 7. State Architecture (state) + +**Impact:** MEDIUM +**Description:** Ground truth principles for state variables and derived values. + +## 8. React Compiler (react-compiler) + +**Impact:** MEDIUM +**Description:** Compatibility patterns for React Compiler with React Native and +Reanimated. + +## 9. User Interface (ui) + +**Impact:** MEDIUM +**Description:** Native UI patterns for images, menus, modals, styling with +NativeWind, and platform-consistent interfaces. + +## 10. JavaScript (js) + +**Impact:** LOW +**Description:** Micro-optimizations like hoisting expensive object creation. + +## 11. Fonts (fonts) + +**Impact:** LOW +**Description:** Native font loading for improved performance. diff --git a/.github/skills/react-native-skills/rules/_template.md b/.github/skills/react-native-skills/rules/_template.md new file mode 100644 index 0000000..1e9e707 --- /dev/null +++ b/.github/skills/react-native-skills/rules/_template.md @@ -0,0 +1,28 @@ +--- +title: Rule Title Here +impact: MEDIUM +impactDescription: Optional description of impact (e.g., "20-50% improvement") +tags: tag1, tag2 +--- + +## Rule Title Here + +**Impact: MEDIUM (optional impact description)** + +Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications. + +**Incorrect (description of what's wrong):** + +```typescript +// Bad code example here +const bad = example() +``` + +**Correct (description of what's right):** + +```typescript +// Good code example here +const good = example() +``` + +Reference: [Link to documentation or resource](https://example.com) diff --git a/.github/skills/react-native-skills/rules/animation-derived-value.md b/.github/skills/react-native-skills/rules/animation-derived-value.md new file mode 100644 index 0000000..310928a --- /dev/null +++ b/.github/skills/react-native-skills/rules/animation-derived-value.md @@ -0,0 +1,53 @@ +--- +title: Prefer useDerivedValue Over useAnimatedReaction +impact: MEDIUM +impactDescription: cleaner code, automatic dependency tracking +tags: animation, reanimated, derived-value +--- + +## Prefer useDerivedValue Over useAnimatedReaction + +When deriving a shared value from another, use `useDerivedValue` instead of +`useAnimatedReaction`. Derived values are declarative, automatically track +dependencies, and return a value you can use directly. Animated reactions are +for side effects, not derivations. + +**Incorrect (useAnimatedReaction for derivation):** + +```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`). + +Reference: +[Reanimated useDerivedValue](https://docs.swmansion.com/react-native-reanimated/docs/core/useDerivedValue) diff --git a/.github/skills/react-native-skills/rules/animation-gesture-detector-press.md b/.github/skills/react-native-skills/rules/animation-gesture-detector-press.md new file mode 100644 index 0000000..87c6782 --- /dev/null +++ b/.github/skills/react-native-skills/rules/animation-gesture-detector-press.md @@ -0,0 +1,95 @@ +--- +title: Use GestureDetector for Animated Press States +impact: MEDIUM +impactDescription: UI thread animations, smoother press feedback +tags: animation, gestures, press, reanimated +--- + +## Use GestureDetector for Animated Press States + +For animated press states (scale, opacity on press), use `GestureDetector` with +`Gesture.Tap()` and shared values instead of Pressable's +`onPressIn`/`onPressOut`. Gesture callbacks run on the UI thread as worklets—no +JS thread round-trip for press animations. + +**Incorrect (Pressable with JS thread callbacks):** + +```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 ( + (scale.value = withTiming(0.95))} + onPressOut={() => (scale.value = withTiming(1))} + > + + Press me + + + ) +} +``` + +**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 ( + + + Press me + + + ) +} +``` + +Store the press **state** (0 or 1), then derive the scale via `interpolate`. +This keeps the shared value as ground truth. Use `runOnJS` to call JS functions +from worklets. Use `.set()` and `.get()` for React Compiler compatibility. + +Reference: +[Gesture Handler Tap Gesture](https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture) diff --git a/.github/skills/react-native-skills/rules/animation-gpu-properties.md b/.github/skills/react-native-skills/rules/animation-gpu-properties.md new file mode 100644 index 0000000..5fda095 --- /dev/null +++ b/.github/skills/react-native-skills/rules/animation-gpu-properties.md @@ -0,0 +1,65 @@ +--- +title: Animate Transform and Opacity Instead of Layout Properties +impact: HIGH +impactDescription: GPU-accelerated animations, no layout recalculation +tags: animation, performance, reanimated, transform, opacity +--- + +## Animate Transform and Opacity Instead of Layout Properties + +Avoid animating `width`, `height`, `top`, `left`, `margin`, or `padding`. These trigger layout recalculation on every frame. Instead, use `transform` (scale, translate) and `opacity` which run on the GPU without triggering layout. + +**Incorrect (animates height, triggers layout every frame):** + +```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 {children} +} +``` + +**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 ( + + {children} + + ) +} +``` + +**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 {children} +} +``` + +GPU-accelerated properties: `transform` (translate, scale, rotate), `opacity`. Everything else triggers layout. diff --git a/.github/skills/react-native-skills/rules/fonts-config-plugin.md b/.github/skills/react-native-skills/rules/fonts-config-plugin.md new file mode 100644 index 0000000..39aa014 --- /dev/null +++ b/.github/skills/react-native-skills/rules/fonts-config-plugin.md @@ -0,0 +1,71 @@ +--- +title: Load fonts natively at build time +impact: LOW +impactDescription: fonts available at launch, no async loading +tags: fonts, expo, performance, config-plugin +--- + +## Use Expo Config Plugin for Font Loading + +Use the `expo-font` config plugin to embed fonts at build time instead of +`useFonts` or `Font.loadAsync`. Embedded fonts are more efficient. + +**Incorrect (async font loading):** + +```tsx +import { useFonts } from 'expo-font' +import { Text, View } from 'react-native' + +function App() { + const [fontsLoaded] = useFonts({ + 'Geist-Bold': require('./assets/fonts/Geist-Bold.otf'), + }) + + if (!fontsLoaded) { + return null + } + + return ( + + Hello + + ) +} +``` + +**Correct (config plugin, fonts embedded at build):** + +```json +// app.json +{ + "expo": { + "plugins": [ + [ + "expo-font", + { + "fonts": ["./assets/fonts/Geist-Bold.otf"] + } + ] + ] + } +} +``` + +```tsx +import { Text, View } from 'react-native' + +function App() { + // No loading state needed—font is already available + return ( + + Hello + + ) +} +``` + +After adding fonts to the config plugin, run `npx expo prebuild` and rebuild the +native app. + +Reference: +[Expo Font Documentation](https://docs.expo.dev/versions/latest/sdk/font/) diff --git a/.github/skills/react-native-skills/rules/js-hoist-intl.md b/.github/skills/react-native-skills/rules/js-hoist-intl.md new file mode 100644 index 0000000..9af1c35 --- /dev/null +++ b/.github/skills/react-native-skills/rules/js-hoist-intl.md @@ -0,0 +1,61 @@ +--- +title: Hoist Intl Formatter Creation +impact: LOW-MEDIUM +impactDescription: avoids expensive object recreation +tags: javascript, intl, optimization, memoization +--- + +## Hoist Intl Formatter Creation + +Don't create `Intl.DateTimeFormat`, `Intl.NumberFormat`, or +`Intl.RelativeTimeFormat` inside render or loops. These are expensive to +instantiate. Hoist to module scope when the locale/options are static. + +**Incorrect (new formatter every render):** + +```tsx +function Price({ amount }: { amount: number }) { + const formatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + }) + return {formatter.format(amount)} +} +``` + +**Correct (hoisted to module scope):** + +```tsx +const currencyFormatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', +}) + +function Price({ amount }: { amount: number }) { + return {currencyFormatter.format(amount)} +} +``` + +**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. diff --git a/.github/skills/react-native-skills/rules/list-performance-callbacks.md b/.github/skills/react-native-skills/rules/list-performance-callbacks.md new file mode 100644 index 0000000..a0b3913 --- /dev/null +++ b/.github/skills/react-native-skills/rules/list-performance-callbacks.md @@ -0,0 +1,44 @@ +--- +title: Hoist callbacks to the root of lists +impact: MEDIUM +impactDescription: Fewer re-renders and faster lists +tags: tag1, tag2 +--- + +## List performance callbacks + +**Impact: HIGH (Fewer re-renders and faster lists)** + +When passing callback functions to list items, create a single instance of the +callback at the root of the list. Items should then call it with a unique +identifier. + +**Incorrect (creates a new callback on each render):** + +```typescript +return ( + { + // bad: creates a new callback on each render + const onPress = () => handlePress(item.id) + return + }} + /> +) +``` + +**Correct (a single function instance passed to each item):** + +```typescript +const onPress = useCallback(() => handlePress(item.id), [handlePress, item.id]) + +return ( + ( + + )} + /> +) +``` + +Reference: [Link to documentation or resource](https://example.com) diff --git a/.github/skills/react-native-skills/rules/list-performance-function-references.md b/.github/skills/react-native-skills/rules/list-performance-function-references.md new file mode 100644 index 0000000..9721929 --- /dev/null +++ b/.github/skills/react-native-skills/rules/list-performance-function-references.md @@ -0,0 +1,132 @@ +--- +title: Optimize List Performance with Stable Object References +impact: CRITICAL +impactDescription: virtualization relies on reference stability +tags: lists, performance, flatlist, virtualization +--- + +## Optimize List Performance with Stable Object References + +Don't map or filter data before passing to virtualized lists. Virtualization +relies on object reference stability to know what changed—new references cause +full re-renders of all visible items. Attempt to prevent frequent renders at the +list-parent level. + +Where needed, use context selectors within list items. + +**Incorrect (creates new object references on every keystroke):** + +```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 ( + <> + + } + /> + + ) +} +``` + +**Correct (stable references, transform inside items):** + +```tsx +const renderItem = ({ item }) => + +function DomainSearch() { + const { data: tlds } = useTlds() + + return ( + + ) +} + +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 {domain} +} +``` + +**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: + +```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 +``` + +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 +const useSearchStore = create<{ keyword: string }>(() => ({ keyword: '' })) + +function DomainSearch() { + const { data: tlds } = useTlds() + + return ( + <> + + } + /> + + ) +} + +function DomainItem({ tld }: { tld: Tld }) { + // Select only what you need—component only re-renders when keyword changes + const keyword = useSearchStore((s) => s.keyword) + const domain = `${keyword}.${tld.name}` + return {domain} +} +``` + +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: + +```tsx +function DomainItemFavoriteButton({ tld }: { tld: Tld }) { + const isFavorited = useFavoritesStore((s) => s.favorites.has(tld.id)) + return +} +``` + +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. diff --git a/.github/skills/react-native-skills/rules/list-performance-images.md b/.github/skills/react-native-skills/rules/list-performance-images.md new file mode 100644 index 0000000..75a3baf --- /dev/null +++ b/.github/skills/react-native-skills/rules/list-performance-images.md @@ -0,0 +1,53 @@ +--- +title: Use Compressed Images in Lists +impact: HIGH +impactDescription: faster load times, less memory +tags: lists, images, performance, optimization +--- + +## Use Compressed Images in Lists + +Always load compressed, appropriately-sized images in lists. Full-resolution +images consume excessive memory and cause scroll jank. Request thumbnails from +your server or use an image CDN with resize parameters. + +**Incorrect (full-resolution images):** + +```tsx +function ProductItem({ product }: { product: Product }) { + return ( + + {/* 4000x3000 image loaded for a 100x100 thumbnail */} + + {product.name} + + ) +} +``` + +**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 ( + + + {product.name} + + ) +} +``` + +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. diff --git a/.github/skills/react-native-skills/rules/list-performance-inline-objects.md b/.github/skills/react-native-skills/rules/list-performance-inline-objects.md new file mode 100644 index 0000000..d5b6514 --- /dev/null +++ b/.github/skills/react-native-skills/rules/list-performance-inline-objects.md @@ -0,0 +1,97 @@ +--- +title: Avoid Inline Objects in renderItem +impact: HIGH +impactDescription: prevents unnecessary re-renders of memoized list items +tags: lists, performance, flatlist, virtualization, memo +--- + +## Avoid Inline Objects in renderItem + +Don't create new objects inside `renderItem` to pass as props. Inline objects +create new references on every render, breaking memoization. Pass primitive +values directly from `item` instead. + +**Incorrect (inline object breaks memoization):** + +```tsx +function UserList({ users }: { users: User[] }) { + return ( + ( + + )} + /> + ) +} +``` + +**Incorrect (inline style object):** + +```tsx +renderItem={({ item }) => ( + +)} +``` + +**Correct (pass item directly or primitives):** + +```tsx +function UserList({ users }: { users: User[] }) { + return ( + ( + // Good: pass the item directly + + )} + /> + ) +} +``` + +**Correct (pass primitives, derive inside child):** + +```tsx +renderItem={({ item }) => ( + +)} + +const UserRow = memo(function UserRow({ id, name, isActive }: Props) { + // Good: derive style inside memoized component + const backgroundColor = isActive ? 'green' : 'gray' + return {/* ... */} +}) +``` + +**Correct (hoist static styles in module scope):** + +```tsx +const activeStyle = { backgroundColor: 'green' } +const inactiveStyle = { backgroundColor: 'gray' } + +renderItem={({ item }) => ( + +)} +``` + +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. diff --git a/.github/skills/react-native-skills/rules/list-performance-item-expensive.md b/.github/skills/react-native-skills/rules/list-performance-item-expensive.md new file mode 100644 index 0000000..f617a76 --- /dev/null +++ b/.github/skills/react-native-skills/rules/list-performance-item-expensive.md @@ -0,0 +1,94 @@ +--- +title: Keep List Items Lightweight +impact: HIGH +impactDescription: reduces render time for visible items during scroll +tags: lists, performance, virtualization, hooks +--- + +## Keep List Items Lightweight + +List items should be as inexpensive as possible to render. Minimize hooks, avoid +queries, and limit React Context access. Virtualized lists render many items +during scroll—expensive items cause jank. + +**Incorrect (heavy list item):** + +```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 {/* ... */} +} +``` + +**Correct (lightweight list item):** + +```tsx +function ProductRow({ name, price, imageUrl }: Props) { + // Good: receives only primitives, minimal hooks + return ( + + + {name} + {price} + + ) +} +``` + +**Move data fetching to parent:** + +```tsx +// Parent fetches all data once +function ProductList() { + const { data: products } = useQuery(['products'], fetchProducts) + + return ( + ( + + )} + /> + ) +} +``` + +**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. diff --git a/.github/skills/react-native-skills/rules/list-performance-item-memo.md b/.github/skills/react-native-skills/rules/list-performance-item-memo.md new file mode 100644 index 0000000..634935e --- /dev/null +++ b/.github/skills/react-native-skills/rules/list-performance-item-memo.md @@ -0,0 +1,82 @@ +--- +title: Pass Primitives to List Items for Memoization +impact: HIGH +impactDescription: enables effective memo() comparison +tags: lists, performance, memo, primitives +--- + +## Pass Primitives to List Items for Memoization + +When possible, pass only primitive values (strings, numbers, booleans) as props +to list item components. Primitives enable shallow comparison in `memo()` to +work correctly, skipping re-renders when values haven't changed. + +**Incorrect (object prop requires deep comparison):** + +```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 {user.name} +}) + +renderItem={({ 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 {name} +}) + +renderItem={({ item }) => ( + +)} +``` + +**Pass only what you need:** + +```tsx +// Incorrect: passing entire item when you only need name + + +// Correct: pass only the fields the component uses + +``` + +**For callbacks, hoist or use item ID:** + +```tsx +// Incorrect: inline function creates new reference + handlePress(item.id)} /> + +// Correct: pass ID, handle in child + + +const UserRow = memo(function UserRow({ id, name }: Props) { + const handlePress = useCallback(() => { + // use id here + }, [id]) + return {name} +}) +``` + +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. diff --git a/.github/skills/react-native-skills/rules/list-performance-item-types.md b/.github/skills/react-native-skills/rules/list-performance-item-types.md new file mode 100644 index 0000000..1027e4e --- /dev/null +++ b/.github/skills/react-native-skills/rules/list-performance-item-types.md @@ -0,0 +1,104 @@ +--- +title: Use Item Types for Heterogeneous Lists +impact: HIGH +impactDescription: efficient recycling, less layout thrashing +tags: list, performance, recycling, heterogeneous, LegendList +--- + +## Use Item Types for Heterogeneous Lists + +When a list has different item layouts (messages, images, headers, etc.), use a +`type` field on each item and provide `getItemType` to the list. This puts items +into separate recycling pools so a message component never gets recycled into an +image component. + +**Incorrect (single component with conditionals):** + +```tsx +type Item = { id: string; text?: string; imageUrl?: string; isHeader?: boolean } + +function ListItem({ item }: { item: Item }) { + if (item.isHeader) { + return + } + if (item.imageUrl) { + return + } + return +} + +function Feed({ items }: { items: Item[] }) { + return ( + } + recycleItems + /> + ) +} +``` + +**Correct (typed items with separate components):** + +```tsx +type HeaderItem = { id: string; type: 'header'; title: string } +type MessageItem = { id: string; type: 'message'; text: string } +type ImageItem = { id: string; type: 'image'; url: string } +type FeedItem = HeaderItem | MessageItem | ImageItem + +function Feed({ items }: { items: FeedItem[] }) { + return ( + item.id} + getItemType={(item) => item.type} + renderItem={({ item }) => { + switch (item.type) { + case 'header': + return + case 'message': + return + case 'image': + return + } + }} + recycleItems + /> + ) +} +``` + +**Why this matters:** + +- **Recycling efficiency**: Items with the same type share a recycling pool +- **No layout thrashing**: A header never recycles into an image cell +- **Type safety**: TypeScript can narrow the item type in each branch +- **Better size estimation**: Use `getEstimatedItemSize` with `itemType` for + accurate estimates per type + +```tsx + item.id} + getItemType={(item) => item.type} + getEstimatedItemSize={(index, item, itemType) => { + switch (itemType) { + case 'header': + return 48 + case 'message': + return 72 + case 'image': + return 300 + default: + return 72 + } + }} + renderItem={({ item }) => { + /* ... */ + }} + recycleItems +/> +``` + +Reference: +[LegendList getItemType](https://legendapp.com/open-source/list/api/props/#getitemtype-v2) diff --git a/.github/skills/react-native-skills/rules/list-performance-virtualize.md b/.github/skills/react-native-skills/rules/list-performance-virtualize.md new file mode 100644 index 0000000..8a393ba --- /dev/null +++ b/.github/skills/react-native-skills/rules/list-performance-virtualize.md @@ -0,0 +1,67 @@ +--- +title: Use a List Virtualizer for Any List +impact: HIGH +impactDescription: reduced memory, faster mounts +tags: lists, performance, virtualization, scrollview +--- + +## Use a List Virtualizer for Any List + +Use a list virtualizer like LegendList or FlashList instead of ScrollView with +mapped children—even for short lists. Virtualizers only render visible items, +reducing memory usage and mount time. ScrollView renders all children upfront, +which gets expensive quickly. + +**Incorrect (ScrollView renders all items at once):** + +```tsx +function Feed({ items }: { items: Item[] }) { + return ( + + {items.map((item) => ( + + ))} + + ) +} +// 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 ( + } + 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 ( + } + keyExtractor={(item) => item.id} + /> + ) +} +``` + +Benefits apply to any screen with scrollable content—profiles, settings, feeds, +search results. Default to virtualization. diff --git a/.github/skills/react-native-skills/rules/navigation-native-navigators.md b/.github/skills/react-native-skills/rules/navigation-native-navigators.md new file mode 100644 index 0000000..ad60a54 --- /dev/null +++ b/.github/skills/react-native-skills/rules/navigation-native-navigators.md @@ -0,0 +1,153 @@ +--- +title: Use Expo Router for File-Based Navigation +impact: HIGH +impactDescription: native performance, platform-appropriate UI +tags: navigation, expo-router, native-stack, tabs, ios, android +--- + +## Use Expo Router for File-Based Navigation + +Use Expo Router for file-based navigation. Expo Router uses native navigators +under the hood (UINavigationController on iOS, Fragment on Android) for better +performance and native behavior. + +### Stack Navigation + +Expo Router uses native stack by default: + +```tsx +// app/_layout.tsx +import { Stack } from 'expo-router' + +export default function RootLayout() { + return ( + + + + + ) +} +``` + +### Tab Navigation + +Use the Tabs component for bottom tab navigation: + +```tsx +// app/(tabs)/_layout.tsx +import { Tabs } from 'expo-router' + +export default function TabLayout() { + return ( + + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + + ) +} +``` + +### Modal Screens + +Use presentation: 'modal' for modal screens: + +```tsx + +``` + +### Prefer Native Header Options Over Custom Components + +**Incorrect (custom header component):** + +```tsx + , + }} +/> +``` + +**Correct (native header options):** + +```tsx + +``` + +Native headers support iOS large titles, search bars, blur effects, and proper +safe area handling automatically. + +### Programmatic Navigation + +```tsx +import { router } from 'expo-router' + +// Navigate to a screen +router.push('/transaction-details') + +// Navigate with params +router.push({ + pathname: '/transaction-details', + params: { txid: '...' }, +}) + +// Go back +router.back() + +// Replace current screen +router.replace('/home') +``` + +### Why Expo Router + +- **File-based routing**: Routes are defined by file structure +- **Native performance**: Uses native stack and tab navigators +- **Type safety**: Full TypeScript support for routes +- **Deep linking**: Built-in deep linking support +- **Platform behavior**: Automatic iOS/Android platform conventions + +Reference: + +- [Expo Router Documentation](https://docs.expo.dev/router/introduction/) +- [Expo Router Native Tabs](https://docs.expo.dev/router/advanced/native-tabs/) diff --git a/.github/skills/react-native-skills/rules/react-compiler-destructure-functions.md b/.github/skills/react-native-skills/rules/react-compiler-destructure-functions.md new file mode 100644 index 0000000..f76c25a --- /dev/null +++ b/.github/skills/react-native-skills/rules/react-compiler-destructure-functions.md @@ -0,0 +1,50 @@ +--- +title: Destructure Functions Early in Render (React Compiler) +impact: HIGH +impactDescription: stable references, fewer re-renders +tags: rerender, hooks, performance, react-compiler +--- + +## Destructure Functions Early in Render + +This rule is only applicable if you are using the React Compiler. + +Destructure functions from hooks at the top of render scope. Never dot into +objects to call functions. Destructured functions are stable references; dotting +creates new references and breaks memoization. + +**Incorrect (dotting into object):** + +```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 +} +``` + +**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 +} +``` diff --git a/.github/skills/react-native-skills/rules/react-compiler-reanimated-shared-values.md b/.github/skills/react-native-skills/rules/react-compiler-reanimated-shared-values.md new file mode 100644 index 0000000..0dcbaf4 --- /dev/null +++ b/.github/skills/react-native-skills/rules/react-compiler-reanimated-shared-values.md @@ -0,0 +1,48 @@ +--- +title: Use .get() and .set() for Reanimated Shared Values (not .value) +impact: LOW +impactDescription: required for React Compiler compatibility +tags: reanimated, react-compiler, shared-values +--- + +## Use .get() and .set() for Shared Values with React Compiler + +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