From fa74127ee569f761ed1b27db3bbf3fb06f3b7367 Mon Sep 17 00:00:00 2001 From: James Pepper Date: Wed, 28 Jan 2026 09:01:31 +0000 Subject: [PATCH 1/2] Add React Native skills guide for agents and LLMs Introduces a comprehensive performance and best practices guide for React Native, targeting AI agents and LLMs. The guide covers 35+ rules across rendering, list performance, animation, navigation, state management, UI, and more, with detailed explanations and code examples for each rule. --- .github/skills/react-native-skills/AGENTS.md | 2897 +++++++++++++++++ .github/skills/react-native-skills/README.md | 165 + .github/skills/react-native-skills/SKILL.md | 121 + .../skills/react-native-skills/metadata.json | 16 + .../react-native-skills/rules/_sections.md | 86 + .../react-native-skills/rules/_template.md | 28 + .../rules/animation-derived-value.md | 53 + .../rules/animation-gesture-detector-press.md | 95 + .../rules/animation-gpu-properties.md | 65 + .../design-system-compound-components.md | 66 + .../rules/fonts-config-plugin.md | 71 + .../rules/imports-design-system-folder.md | 68 + .../rules/js-hoist-intl.md | 61 + .../rules/list-performance-callbacks.md | 44 + .../list-performance-function-references.md | 132 + .../rules/list-performance-images.md | 53 + .../rules/list-performance-inline-objects.md | 97 + .../rules/list-performance-item-expensive.md | 94 + .../rules/list-performance-item-memo.md | 82 + .../rules/list-performance-item-types.md | 104 + .../rules/list-performance-virtualize.md | 67 + .../rules/monorepo-native-deps-in-app.md | 46 + .../monorepo-single-dependency-versions.md | 63 + .../rules/navigation-native-navigators.md | 188 ++ .../react-compiler-destructure-functions.md | 50 + ...react-compiler-reanimated-shared-values.md | 48 + .../rules/react-state-dispatcher.md | 91 + .../rules/react-state-fallback.md | 56 + .../rules/react-state-minimize.md | 65 + .../rules/rendering-no-falsy-and.md | 74 + .../rules/rendering-text-in-text-component.md | 36 + .../rules/scroll-position-no-state.md | 82 + .../rules/state-ground-truth.md | 80 + .../rules/ui-expo-image.md | 66 + .../rules/ui-image-gallery.md | 104 + .../rules/ui-measure-views.md | 78 + .../react-native-skills/rules/ui-menus.md | 174 + .../rules/ui-native-modals.md | 77 + .../react-native-skills/rules/ui-pressable.md | 61 + .../rules/ui-safe-area-scroll.md | 65 + .../rules/ui-scrollview-content-inset.md | 45 + .../react-native-skills/rules/ui-styling.md | 87 + 42 files changed, 6101 insertions(+) create mode 100644 .github/skills/react-native-skills/AGENTS.md create mode 100644 .github/skills/react-native-skills/README.md create mode 100644 .github/skills/react-native-skills/SKILL.md create mode 100644 .github/skills/react-native-skills/metadata.json create mode 100644 .github/skills/react-native-skills/rules/_sections.md create mode 100644 .github/skills/react-native-skills/rules/_template.md create mode 100644 .github/skills/react-native-skills/rules/animation-derived-value.md create mode 100644 .github/skills/react-native-skills/rules/animation-gesture-detector-press.md create mode 100644 .github/skills/react-native-skills/rules/animation-gpu-properties.md create mode 100644 .github/skills/react-native-skills/rules/design-system-compound-components.md create mode 100644 .github/skills/react-native-skills/rules/fonts-config-plugin.md create mode 100644 .github/skills/react-native-skills/rules/imports-design-system-folder.md create mode 100644 .github/skills/react-native-skills/rules/js-hoist-intl.md create mode 100644 .github/skills/react-native-skills/rules/list-performance-callbacks.md create mode 100644 .github/skills/react-native-skills/rules/list-performance-function-references.md create mode 100644 .github/skills/react-native-skills/rules/list-performance-images.md create mode 100644 .github/skills/react-native-skills/rules/list-performance-inline-objects.md create mode 100644 .github/skills/react-native-skills/rules/list-performance-item-expensive.md create mode 100644 .github/skills/react-native-skills/rules/list-performance-item-memo.md create mode 100644 .github/skills/react-native-skills/rules/list-performance-item-types.md create mode 100644 .github/skills/react-native-skills/rules/list-performance-virtualize.md create mode 100644 .github/skills/react-native-skills/rules/monorepo-native-deps-in-app.md create mode 100644 .github/skills/react-native-skills/rules/monorepo-single-dependency-versions.md create mode 100644 .github/skills/react-native-skills/rules/navigation-native-navigators.md create mode 100644 .github/skills/react-native-skills/rules/react-compiler-destructure-functions.md create mode 100644 .github/skills/react-native-skills/rules/react-compiler-reanimated-shared-values.md create mode 100644 .github/skills/react-native-skills/rules/react-state-dispatcher.md create mode 100644 .github/skills/react-native-skills/rules/react-state-fallback.md create mode 100644 .github/skills/react-native-skills/rules/react-state-minimize.md create mode 100644 .github/skills/react-native-skills/rules/rendering-no-falsy-and.md create mode 100644 .github/skills/react-native-skills/rules/rendering-text-in-text-component.md create mode 100644 .github/skills/react-native-skills/rules/scroll-position-no-state.md create mode 100644 .github/skills/react-native-skills/rules/state-ground-truth.md create mode 100644 .github/skills/react-native-skills/rules/ui-expo-image.md create mode 100644 .github/skills/react-native-skills/rules/ui-image-gallery.md create mode 100644 .github/skills/react-native-skills/rules/ui-measure-views.md create mode 100644 .github/skills/react-native-skills/rules/ui-menus.md create mode 100644 .github/skills/react-native-skills/rules/ui-native-modals.md create mode 100644 .github/skills/react-native-skills/rules/ui-pressable.md create mode 100644 .github/skills/react-native-skills/rules/ui-safe-area-scroll.md create mode 100644 .github/skills/react-native-skills/rules/ui-scrollview-content-inset.md create mode 100644 .github/skills/react-native-skills/rules/ui-styling.md diff --git a/.github/skills/react-native-skills/AGENTS.md b/.github/skills/react-native-skills/AGENTS.md new file mode 100644 index 0000000..d263eb9 --- /dev/null +++ b/.github/skills/react-native-skills/AGENTS.md @@ -0,0 +1,2897 @@ +# 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) + - 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 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 && }` 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} + + ) +} +``` + +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 ``. 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, 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 ( + ( + + )} + /> + ) +} +``` + +**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. + +### 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 ( + { + // 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: [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 {/* ... */} +} +``` + +**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. + +### 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 ( + <> + + } + /> + + ) +} +``` + +**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:** + +```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 +``` + +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 +} +``` + +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 {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. + +### 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 ( + + {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. + +### 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 ( + + {/* 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. + +### 2.8 Use Item Types for Heterogeneous Lists + +**Impact: HIGH (efficient recycling, less layout thrashing)** + +When a list has different item layouts (messages, images, headers, etc.), use a + +`type` field on each item and provide `getItemType` to the list. This puts items + +into separate recycling pools so a message component never gets recycled into an + +image component. + +[LegendList getItemType](https://legendapp.com/open-source/list/api/props/#getitemtype-v2) + +**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:** + +```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 +/> +``` + +- **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 + +--- + +## 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 {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. + +### 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 ( + (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. + +--- + +## 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) => { + setScrollY(e.nativeEvent.contentOffset.y) // re-renders on every frame + } + + return +} +``` + +**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 ( + + ) +} +``` + +**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) => { + scrollY.current = e.nativeEvent.contentOffset.y // no re-render + } + + return +} +``` + +--- + +## 5. Navigation + +**Impact: HIGH** + +Using native navigators for stack and tab navigation instead of +JS-based alternatives. + +### 5.1 Use Native Navigators for Navigation + +**Impact: HIGH (native performance, platform-appropriate UI)** + +Always use native navigators instead of JS-based ones. Native navigators use + +platform APIs (UINavigationController on iOS, Fragment on Android) for better + +performance and native behavior. + +**For stacks:** Use `@react-navigation/native-stack` or expo-router's default + +stack (which uses native-stack). Avoid `@react-navigation/stack`. + +**For tabs:** Use `react-native-bottom-tabs` (native) or expo-router's native + +tabs. Avoid `@react-navigation/bottom-tabs` when native feel matters. + +- [React Navigation Native Stack](https://reactnavigation.org/docs/native-stack-navigator) + +- [React Native Bottom Tabs with React Navigation](https://oss.callstack.com/react-native-bottom-tabs/docs/guides/usage-with-react-navigation) + +- [React Native Bottom Tabs with Expo Router](https://oss.callstack.com/react-native-bottom-tabs/docs/guides/usage-with-expo-router) + +- [Expo Router Native Tabs](https://docs.expo.dev/router/advanced/native-tabs) + +**Incorrect: JS stack navigator** + +```tsx +import { createStackNavigator } from '@react-navigation/stack' + +const Stack = createStackNavigator() + +function App() { + return ( + + + + + ) +} +``` + +**Correct: native stack with react-navigation** + +```tsx +import { createNativeStackNavigator } from '@react-navigation/native-stack' + +const Stack = createNativeStackNavigator() + +function App() { + return ( + + + + + ) +} +``` + +**Correct: expo-router uses native stack by default** + +```tsx +// app/_layout.tsx +import { Stack } from 'expo-router' + +export default function Layout() { + return +} +``` + +**Incorrect: JS bottom tabs** + +```tsx +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' + +const Tab = createBottomTabNavigator() + +function App() { + return ( + + + + + ) +} +``` + +**Correct: native bottom tabs with react-navigation** + +```tsx +import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation' + +const Tab = createNativeBottomTabNavigator() + +function App() { + return ( + + ({ sfSymbol: 'house' }), + }} + /> + ({ sfSymbol: 'gear' }), + }} + /> + + ) +} +``` + +**Correct: expo-router native tabs** + +```tsx +// app/(tabs)/_layout.tsx +import { NativeTabs } from 'expo-router/unstable-native-tabs' + +export default function TabLayout() { + return ( + + + Home + + + + Settings + + + + ) +} +``` + +On iOS, native tabs automatically enable `contentInsetAdjustmentBehavior` on the + +first `ScrollView` at the root of each tab screen, so content scrolls correctly + +behind the translucent tab bar. If you need to disable this, use + +`disableAutomaticContentInsets` on the trigger. + +**Incorrect: custom header component** + +```tsx + , + }} +/> +``` + +**Correct: native header options** + +```tsx + +``` + +Native headers support iOS large titles, search bars, blur effects, and proper + +safe area handling automatically. + +- **Performance**: Native transitions and gestures run on the UI thread + +- **Platform behavior**: Automatic iOS large titles, Android material design + +- **System integration**: Scroll-to-top on tab tap, PiP avoidance, proper safe + + areas + +- **Accessibility**: Platform accessibility features work automatically + +--- + +## 6. React State + +**Impact: MEDIUM** + +Patterns for managing React state to avoid stale closures and +unnecessary re-renders. + +### 6.1 Minimize State Variables and Derive Values + +**Impact: MEDIUM (fewer re-renders, less state drift)** + +Use the fewest state variables possible. If a value can be computed from existing state or props, derive it during render instead of storing it in state. Redundant state causes unnecessary re-renders and can drift out of sync. + +**Incorrect: redundant state** + +```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 ( + + {itemCount} items + Total: ${total} + + ) +} +``` + +**Correct: derived values** + +```tsx +function Cart({ items }: { items: Item[] }) { + const total = items.reduce((sum, item) => sum + item.price, 0) + const itemCount = items.length + + return ( + + {itemCount} items + Total: ${total} + + ) +} +``` + +**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) + +### 6.2 Use fallback state instead of initialState + +**Impact: MEDIUM (reactive fallbacks without syncing)** + +Use `undefined` as initial state and nullish coalescing (`??`) to fall back to + +parent or server values. State represents user intent only—`undefined` means + +"user hasn't chosen yet." This enables reactive fallbacks that update when the + +source changes, not just on initial render. + +**Incorrect: syncs state, loses reactivity** + +```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 +} +``` + +**Correct: state is user intent, reactive fallback** + +```tsx +type Props = { fallbackEnabled: boolean } + +function Toggle({ fallbackEnabled }: Props) { + const [_enabled, setEnabled] = useState(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 +} +``` + +**With server data:** + +```tsx +function ProfileForm({ data }: { data: User }) { + const [_theme, setTheme] = useState(undefined) + const theme = _theme ?? data.theme + // Shows server value until user overrides + // Server refetch updates the fallback automatically + + return +} +``` + +### 6.3 useState Dispatch updaters for State That Depends on Current Value + +**Impact: MEDIUM (avoids stale closures, prevents unnecessary re-renders)** + +When the next state depends on the current state, use a dispatch updater + +(`setState(prev => ...)`) instead of reading the state variable directly in a + +callback. This avoids stale closures and ensures you're comparing against the + +latest value. + +**Incorrect: reads state directly** + +```tsx +const [size, setSize] = useState(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(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(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(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) +} +``` + +--- + +## 7. State Architecture + +**Impact: MEDIUM** + +Ground truth principles for state variables and derived values. + +### 7.1 State Must Represent Ground Truth + +**Impact: HIGH (cleaner logic, easier debugging, single source of truth)** + +State variables—both React `useState` and Reanimated shared values—should + +represent the actual state of something (e.g., `pressed`, `progress`, `isOpen`), + +not derived visual values (e.g., `scale`, `opacity`, `translateY`). Derive + +visual values from state using computation or interpolation. + +**Incorrect: storing the visual output** + +```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. + +--- + +## 8. React Compiler + +**Impact: MEDIUM** + +Compatibility patterns for React Compiler with React Native and +Reanimated. + +### 8.1 Destructure Functions Early in Render (React Compiler) + +**Impact: HIGH (stable references, fewer re-renders)** + +This rule is only applicable if you are using the React Compiler. + +Destructure functions from hooks at the top of render scope. Never dot into + +objects to call functions. Destructured functions are stable references; dotting + +creates new references and breaks memoization. + +**Incorrect: dotting into object** + +```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 +} +``` + +### 8.2 Use .get() and .set() for Reanimated Shared Values (not .value) + +**Impact: LOW (required for React Compiler compatibility)** + +With React Compiler enabled, use `.get()` and `.set()` instead of reading or + +writing `.value` directly on Reanimated shared values. The compiler can't track + +property access—explicit methods ensure correct behavior. + +**Incorrect: breaks with React Compiler** + +```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 + +``` + +**Correct: compound components** + +```tsx +import { Pressable, Text } from 'react-native' + +function Button({ children }: { children: React.ReactNode }) { + return {children} +} + +function ButtonText({ children }: { children: React.ReactNode }) { + return {children} +} + +function ButtonIcon({ children }: { children: React.ReactNode }) { + return <>{children} +} + +// Usage is explicit and composable + + + +``` + +--- + +## 11. Monorepo + +**Impact: LOW** + +Dependency management and native module configuration in +monorepos. + +### 11.1 Install Native Dependencies in App Directory + +**Impact: CRITICAL (required for autolinking to work)** + +In a monorepo, packages with native code must be installed in the native app's + +directory directly. Autolinking only scans the app's `node_modules`—it won't + +find native dependencies installed in other packages. + +**Incorrect: native dep in shared package only** + +```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. + +### 11.2 Use Single Dependency Versions Across Monorepo + +**Impact: MEDIUM (avoids duplicate bundles, version conflicts)** + +Use a single version of each dependency across all packages in your monorepo. + +Prefer exact versions over ranges. Multiple versions cause duplicate code in + +bundles, runtime conflicts, and inconsistent behavior across packages. + +Use a tool like syncpack to enforce this. As a last resort, use yarn resolutions + +or npm overrides. + +**Incorrect: version ranges, multiple versions** + +```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 `~`. + +--- + +## 12. Third-Party Dependencies + +**Impact: LOW** + +Wrapping and re-exporting third-party dependencies for +maintainability. + +### 12.1 Import from Design System Folder + +**Impact: LOW (enables global changes and easy refactoring)** + +Re-export dependencies from a design system folder. App code imports from there, + +not directly from packages. This enables global changes and easy refactoring. + +**Incorrect: imports directly from package** + +```tsx +import { View, Text } from 'react-native' +import { Button } from '@ui/button' + +function Profile() { + return ( + + Hello + + + ) +} +``` + +**Correct: imports from design system** + +```tsx +import { View } from '@/components/view' +import { Text } from '@/components/text' +import { Button } from '@/components/button' + +function Profile() { + return ( + + Hello + + + ) +} +``` + +Start by simply re-exporting. Customize later without changing app code. + +--- + +## 13. JavaScript + +**Impact: LOW** + +Micro-optimizations like hoisting expensive object creation. + +### 13.1 Hoist Intl Formatter Creation + +**Impact: LOW-MEDIUM (avoids expensive object recreation)** + +Don't create `Intl.DateTimeFormat`, `Intl.NumberFormat`, or + +`Intl.RelativeTimeFormat` inside render or loops. These are expensive to + +instantiate. Hoist to module scope when the locale/options are static. + +**Incorrect: new formatter every render** + +```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. + +--- + +## 14. Fonts + +**Impact: LOW** + +Native font loading for improved performance. + +### 14.1 Load fonts natively at build time + +**Impact: LOW (fonts available at launch, no async loading)** + +Use the `expo-font` config plugin to embed fonts at build time instead of + +`useFonts` or `Font.loadAsync`. Embedded fonts are more efficient. + +[Expo Font Documentation](https://docs.expo.dev/versions/latest/sdk/font/) + +**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** + +```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. + +--- + +## References + +1. [https://react.dev](https://react.dev) +2. [https://reactnative.dev](https://reactnative.dev) +3. [https://docs.swmansion.com/react-native-reanimated](https://docs.swmansion.com/react-native-reanimated) +4. [https://docs.swmansion.com/react-native-gesture-handler](https://docs.swmansion.com/react-native-gesture-handler) +5. [https://docs.expo.dev](https://docs.expo.dev) +6. [https://legendapp.com/open-source/legend-list](https://legendapp.com/open-source/legend-list) +7. [https://github.com/nandorojo/galeria](https://github.com/nandorojo/galeria) +8. [https://zeego.dev](https://zeego.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..854db9f --- /dev/null +++ b/.github/skills/react-native-skills/README.md @@ -0,0 +1,165 @@ +# React Native Guidelines + +A structured repository for creating and maintaining React Native Best Practices +optimized for agents and LLMs. + +## Structure + +- `rules/` - Individual rule files (one per rule) + - `_sections.md` - Section metadata (titles, impacts, descriptions) + - `_template.md` - Template for creating new rules + - `area-description.md` - Individual rule files +- `metadata.json` - Document metadata (version, organization, abstract) +- **`AGENTS.md`** - Compiled output (generated) + +## Rules + +### Core Rendering (CRITICAL) + +- `rendering-text-in-text-component.md` - Wrap strings in Text components +- `rendering-no-falsy-and.md` - Avoid falsy && operator in JSX + +### List Performance (HIGH) + +- `list-performance-virtualize.md` - Use virtualized lists (LegendList, + FlashList) +- `list-performance-function-references.md` - Keep stable object 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 instead of layout +- `animation-gesture-detector-press.md` - Use GestureDetector for press + animations +- `animation-derived-value.md` - Prefer useDerivedValue over useAnimatedReaction + +### Scroll Performance (HIGH) + +- `scroll-position-no-state.md` - Never track scroll in useState + +### Navigation (HIGH) + +- `navigation-native-navigators.md` - Use native stack and native tabs + +### React State (MEDIUM) + +- `react-state-dispatcher.md` - Use functional setState updates +- `react-state-fallback.md` - State should represent user intent only +- `react-state-minimize.md` - Minimize state variables, derive values + +### State Architecture (MEDIUM) + +- `state-ground-truth.md` - State must represent ground truth + +### React Compiler (MEDIUM) + +- `react-compiler-destructure-functions.md` - Destructure functions early +- `react-compiler-reanimated-shared-values.md` - Use .get()/.set() for shared + values + +### User Interface (MEDIUM) + +- `ui-expo-image.md` - Use expo-image for optimized images +- `ui-image-gallery.md` - Use Galeria for lightbox/galleries +- `ui-menus.md` - Native dropdown and context menus with Zeego +- `ui-native-modals.md` - Use native Modal with formSheet +- `ui-pressable.md` - Use Pressable instead of TouchableOpacity +- `ui-measure-views.md` - Measuring view dimensions +- `ui-safe-area-scroll.md` - Use contentInsetAdjustmentBehavior +- `ui-scrollview-content-inset.md` - Use contentInset for dynamic spacing +- `ui-styling.md` - Modern styling patterns (gap, boxShadow, gradients) + +### Design System (MEDIUM) + +- `design-system-compound-components.md` - Use compound components + +### Monorepo (LOW) + +- `monorepo-native-deps-in-app.md` - Install native deps in app directory +- `monorepo-single-dependency-versions.md` - Single dependency versions + +### Third-Party Dependencies (LOW) + +- `imports-design-system-folder.md` - Import from design system folder + +### JavaScript (LOW) + +- `js-hoist-intl.md` - Hoist Intl formatter creation + +### Fonts (LOW) + +- `fonts-config-plugin.md` - Load fonts natively at build time + +## Creating a New Rule + +1. Copy `rules/_template.md` to `rules/area-description.md` +2. Choose the appropriate area prefix: + - `rendering-` for Core Rendering + - `list-performance-` for List Performance + - `animation-` for Animation + - `scroll-` for Scroll Performance + - `navigation-` for Navigation + - `react-state-` for React State + - `state-` for State Architecture + - `react-compiler-` for React Compiler + - `ui-` for User Interface + - `design-system-` for Design System + - `monorepo-` for Monorepo + - `imports-` for Third-Party Dependencies + - `js-` for JavaScript + - `fonts-` for Fonts +3. Fill in the frontmatter and content +4. Ensure you have clear examples with explanations + +## Rule File Structure + +Each rule file should follow this 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 of what's wrong):** + +```tsx +// Bad code example +``` +```` + +**Correct (description of what's right):** + +```tsx +// Good code example +``` + +Reference: [Link](https://example.com) + +``` + +## File Naming Convention + +- Files starting with `_` are special (excluded from build) +- Rule files: `area-description.md` (e.g., `animation-gpu-properties.md`) +- Section is automatically inferred from filename prefix +- Rules are sorted alphabetically by title within each section + +## Impact Levels + +- `CRITICAL` - Highest priority, causes crashes or broken UI +- `HIGH` - Significant performance improvements +- `MEDIUM` - Moderate performance improvements +- `LOW` - Incremental improvements +``` diff --git a/.github/skills/react-native-skills/SKILL.md b/.github/skills/react-native-skills/SKILL.md new file mode 100644 index 0000000..7340186 --- /dev/null +++ b/.github/skills/react-native-skills/SKILL.md @@ -0,0 +1,121 @@ +--- +name: vercel-react-native-skills +description: + React Native and Expo best practices for building performant mobile apps. Use + when building React Native components, optimizing list performance, + implementing animations, or working with native modules. Triggers on tasks + involving React Native, Expo, mobile performance, or native platform APIs. +license: MIT +metadata: + author: vercel + version: '1.0.0' +--- + +# React Native Skills + +Comprehensive best practices for React Native and Expo applications. Contains +rules across multiple categories covering performance, animations, UI patterns, +and platform-specific optimizations. + +## When to Apply + +Reference these guidelines when: + +- Building React Native or Expo apps +- Optimizing list and scroll performance +- Implementing animations with Reanimated +- Working with images and media +- Configuring native modules or fonts +- Structuring monorepo projects with native dependencies + +## Rule Categories by Priority + +| Priority | Category | Impact | Prefix | +| -------- | ---------------- | -------- | -------------------- | +| 1 | List Performance | CRITICAL | `list-performance-` | +| 2 | Animation | HIGH | `animation-` | +| 3 | Navigation | HIGH | `navigation-` | +| 4 | UI Patterns | HIGH | `ui-` | +| 5 | State Management | MEDIUM | `react-state-` | +| 6 | Rendering | MEDIUM | `rendering-` | +| 7 | Monorepo | MEDIUM | `monorepo-` | +| 8 | Configuration | LOW | `fonts-`, `imports-` | + +## Quick Reference + +### 1. List Performance (CRITICAL) + +- `list-performance-virtualize` - Use FlashList for large lists +- `list-performance-item-memo` - Memoize list item components +- `list-performance-callbacks` - Stabilize callback references +- `list-performance-inline-objects` - Avoid inline style objects +- `list-performance-function-references` - Extract functions outside render +- `list-performance-images` - Optimize images in lists +- `list-performance-item-expensive` - Move expensive work outside items +- `list-performance-item-types` - Use item types for heterogeneous lists + +### 2. Animation (HIGH) + +- `animation-gpu-properties` - Animate only transform and opacity +- `animation-derived-value` - Use useDerivedValue for computed animations +- `animation-gesture-detector-press` - Use Gesture.Tap instead of Pressable + +### 3. Navigation (HIGH) + +- `navigation-native-navigators` - Use native stack and native tabs over JS navigators + +### 4. UI Patterns (HIGH) + +- `ui-expo-image` - Use expo-image for all images +- `ui-image-gallery` - Use Galeria for image lightboxes +- `ui-pressable` - Use Pressable over TouchableOpacity +- `ui-safe-area-scroll` - Handle safe areas in ScrollViews +- `ui-scrollview-content-inset` - Use contentInset for headers +- `ui-menus` - Use native context menus +- `ui-native-modals` - Use native modals when possible +- `ui-measure-views` - Use onLayout, not measure() +- `ui-styling` - Use StyleSheet.create or Nativewind + +### 5. State Management (MEDIUM) + +- `react-state-minimize` - Minimize state subscriptions +- `react-state-dispatcher` - Use dispatcher pattern for callbacks +- `react-state-fallback` - Show fallback on first render +- `react-compiler-destructure-functions` - Destructure for React Compiler +- `react-compiler-reanimated-shared-values` - Handle shared values with compiler + +### 6. Rendering (MEDIUM) + +- `rendering-text-in-text-component` - Wrap text in Text components +- `rendering-no-falsy-and` - Avoid falsy && for conditional rendering + +### 7. Monorepo (MEDIUM) + +- `monorepo-native-deps-in-app` - Keep native dependencies in app package +- `monorepo-single-dependency-versions` - Use single versions across packages + +### 8. Configuration (LOW) + +- `fonts-config-plugin` - Use config plugins for custom fonts +- `imports-design-system-folder` - Organize design system imports +- `js-hoist-intl` - Hoist Intl object creation + +## How to Use + +Read individual rule files for detailed explanations and code examples: + +``` +rules/list-performance-virtualize.md +rules/animation-gpu-properties.md +``` + +Each rule file contains: + +- Brief explanation of why it matters +- Incorrect code example with explanation +- Correct code example with explanation +- Additional context and references + +## Full Compiled Document + +For the complete guide with all rules expanded: `AGENTS.md` diff --git a/.github/skills/react-native-skills/metadata.json b/.github/skills/react-native-skills/metadata.json new file mode 100644 index 0000000..600eb5b --- /dev/null +++ b/.github/skills/react-native-skills/metadata.json @@ -0,0 +1,16 @@ +{ + "version": "1.0.0", + "organization": "Engineering", + "date": "January 2026", + "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.", + "references": [ + "https://react.dev", + "https://reactnative.dev", + "https://docs.swmansion.com/react-native-reanimated", + "https://docs.swmansion.com/react-native-gesture-handler", + "https://docs.expo.dev", + "https://legendapp.com/open-source/legend-list", + "https://github.com/nandorojo/galeria", + "https://zeego.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..0519cf2 --- /dev/null +++ b/.github/skills/react-native-skills/rules/_sections.md @@ -0,0 +1,86 @@ +# Sections + +This file defines all sections, their ordering, impact levels, and descriptions. +The section ID (in parentheses) is the filename prefix used to group rules. + +--- + +## 1. Core Rendering (rendering) + +**Impact:** CRITICAL +**Description:** Fundamental React Native rendering rules. Violations cause +runtime crashes or broken UI. + +## 2. List Performance (list-performance) + +**Impact:** HIGH +**Description:** Optimizing virtualized lists (FlatList, LegendList, FlashList) +for smooth scrolling and fast updates. + +## 3. Animation (animation) + +**Impact:** HIGH +**Description:** GPU-accelerated animations, Reanimated patterns, and avoiding +render thrashing during gestures. + +## 4. Scroll Performance (scroll) + +**Impact:** HIGH +**Description:** Tracking scroll position without causing render thrashing. + +## 5. Navigation (navigation) + +**Impact:** HIGH +**Description:** Using native navigators for stack and tab navigation instead of +JS-based alternatives. + +## 6. React State (react-state) + +**Impact:** MEDIUM +**Description:** Patterns for managing React state to avoid stale closures and +unnecessary re-renders. + +## 7. State Architecture (state) + +**Impact:** MEDIUM +**Description:** Ground truth principles for state variables and derived values. + +## 8. React Compiler (react-compiler) + +**Impact:** MEDIUM +**Description:** Compatibility patterns for React Compiler with React Native and +Reanimated. + +## 9. User Interface (ui) + +**Impact:** MEDIUM +**Description:** Native UI patterns for images, menus, modals, styling, and +platform-consistent interfaces. + +## 10. Design System (design-system) + +**Impact:** MEDIUM +**Description:** Architecture patterns for building maintainable component +libraries. + +## 11. Monorepo (monorepo) + +**Impact:** LOW +**Description:** Dependency management and native module configuration in +monorepos. + +## 12. Third-Party Dependencies (imports) + +**Impact:** LOW +**Description:** Wrapping and re-exporting third-party dependencies for +maintainability. + +## 13. JavaScript (js) + +**Impact:** LOW +**Description:** Micro-optimizations like hoisting expensive object creation. + +## 14. Fonts (fonts) + +**Impact:** LOW +**Description:** Native font loading for improved performance. 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/design-system-compound-components.md b/.github/skills/react-native-skills/rules/design-system-compound-components.md new file mode 100644 index 0000000..d8239ee --- /dev/null +++ b/.github/skills/react-native-skills/rules/design-system-compound-components.md @@ -0,0 +1,66 @@ +--- +title: Use Compound Components Over Polymorphic Children +impact: MEDIUM +impactDescription: flexible composition, clearer API +tags: design-system, components, composition +--- + +## Use Compound Components Over Polymorphic Children + +Don't create components that can accept a string if they aren't a text node. If +a component can receive a string child, it must be a dedicated `*Text` +component. For components like buttons, which can have both a View (or +Pressable) together with text, use compound components, such a `Button`, +`ButtonText`, and `ButtonIcon`. + +**Incorrect (polymorphic children):** + +```tsx +import { Pressable, Text } from 'react-native' + +type ButtonProps = { + children: string | React.ReactNode + icon?: React.ReactNode +} + +function Button({ children, icon }: ButtonProps) { + return ( + + {icon} + {typeof children === 'string' ? {children} : children} + + ) +} + +// Usage is ambiguous + + +``` + +**Correct (compound components):** + +```tsx +import { Pressable, Text } from 'react-native' + +function Button({ children }: { children: React.ReactNode }) { + return {children} +} + +function ButtonText({ children }: { children: React.ReactNode }) { + return {children} +} + +function ButtonIcon({ children }: { children: React.ReactNode }) { + return <>{children} +} + +// Usage is explicit and composable + + + +``` 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/imports-design-system-folder.md b/.github/skills/react-native-skills/rules/imports-design-system-folder.md new file mode 100644 index 0000000..8466dcb --- /dev/null +++ b/.github/skills/react-native-skills/rules/imports-design-system-folder.md @@ -0,0 +1,68 @@ +--- +title: Import from Design System Folder +impact: LOW +impactDescription: enables global changes and easy refactoring +tags: imports, architecture, design-system +--- + +## Import from Design System Folder + +Re-export dependencies from a design system folder. App code imports from there, +not directly from packages. This enables global changes and easy refactoring. + +**Incorrect (imports directly from package):** + +```tsx +import { View, Text } from 'react-native' +import { Button } from '@ui/button' + +function Profile() { + return ( + + Hello + + + ) +} +``` + +**Correct (imports from design system):** + +```tsx +// components/view.tsx +import { View as RNView } from 'react-native' + +// ideal: pick the props you will actually use to control implementation +export function View( + props: Pick, 'style' | 'children'> +) { + return +} +``` + +```tsx +// components/text.tsx +export { Text } from 'react-native' +``` + +```tsx +// components/button.tsx +export { Button } from '@ui/button' +``` + +```tsx +import { View } from '@/components/view' +import { Text } from '@/components/text' +import { Button } from '@/components/button' + +function Profile() { + return ( + + Hello + + + ) +} +``` + +Start by simply re-exporting. Customize later without changing app code. 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/monorepo-native-deps-in-app.md b/.github/skills/react-native-skills/rules/monorepo-native-deps-in-app.md new file mode 100644 index 0000000..ff85d76 --- /dev/null +++ b/.github/skills/react-native-skills/rules/monorepo-native-deps-in-app.md @@ -0,0 +1,46 @@ +--- +title: Install Native Dependencies in App Directory +impact: CRITICAL +impactDescription: required for autolinking to work +tags: monorepo, native, autolinking, installation +--- + +## Install Native Dependencies in App Directory + +In a monorepo, packages with native code must be installed in the native app's +directory directly. Autolinking only scans the app's `node_modules`—it won't +find native dependencies installed in other packages. + +**Incorrect (native dep in shared package only):** + +``` +packages/ + ui/ + package.json # has react-native-reanimated + app/ + package.json # missing react-native-reanimated +``` + +Autolinking fails—native code not linked. + +**Correct (native dep in app directory):** + +``` +packages/ + ui/ + package.json # has react-native-reanimated + app/ + package.json # also has react-native-reanimated +``` + +```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. diff --git a/.github/skills/react-native-skills/rules/monorepo-single-dependency-versions.md b/.github/skills/react-native-skills/rules/monorepo-single-dependency-versions.md new file mode 100644 index 0000000..1087dfa --- /dev/null +++ b/.github/skills/react-native-skills/rules/monorepo-single-dependency-versions.md @@ -0,0 +1,63 @@ +--- +title: Use Single Dependency Versions Across Monorepo +impact: MEDIUM +impactDescription: avoids duplicate bundles, version conflicts +tags: monorepo, dependencies, installation +--- + +## Use Single Dependency Versions Across Monorepo + +Use a single version of each dependency across all packages in your monorepo. +Prefer exact versions over ranges. Multiple versions cause duplicate code in +bundles, runtime conflicts, and inconsistent behavior across packages. + +Use a tool like syncpack to enforce this. As a last resort, use yarn resolutions +or npm overrides. + +**Incorrect (version ranges, multiple versions):** + +```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 `~`. 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..035c5fd --- /dev/null +++ b/.github/skills/react-native-skills/rules/navigation-native-navigators.md @@ -0,0 +1,188 @@ +--- +title: Use Native Navigators for Navigation +impact: HIGH +impactDescription: native performance, platform-appropriate UI +tags: navigation, react-navigation, expo-router, native-stack, tabs +--- + +## Use Native Navigators for Navigation + +Always use native navigators instead of JS-based ones. Native navigators use +platform APIs (UINavigationController on iOS, Fragment on Android) for better +performance and native behavior. + +**For stacks:** Use `@react-navigation/native-stack` or expo-router's default +stack (which uses native-stack). Avoid `@react-navigation/stack`. + +**For tabs:** Use `react-native-bottom-tabs` (native) or expo-router's native +tabs. Avoid `@react-navigation/bottom-tabs` when native feel matters. + +### Stack Navigation + +**Incorrect (JS stack navigator):** + +```tsx +import { createStackNavigator } from '@react-navigation/stack' + +const Stack = createStackNavigator() + +function App() { + return ( + + + + + ) +} +``` + +**Correct (native stack with react-navigation):** + +```tsx +import { createNativeStackNavigator } from '@react-navigation/native-stack' + +const Stack = createNativeStackNavigator() + +function App() { + return ( + + + + + ) +} +``` + +**Correct (expo-router uses native stack by default):** + +```tsx +// app/_layout.tsx +import { Stack } from 'expo-router' + +export default function Layout() { + return +} +``` + +### Tab Navigation + +**Incorrect (JS bottom tabs):** + +```tsx +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' + +const Tab = createBottomTabNavigator() + +function App() { + return ( + + + + + ) +} +``` + +**Correct (native bottom tabs with react-navigation):** + +```tsx +import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation' + +const Tab = createNativeBottomTabNavigator() + +function App() { + return ( + + ({ sfSymbol: 'house' }), + }} + /> + ({ sfSymbol: 'gear' }), + }} + /> + + ) +} +``` + +**Correct (expo-router native tabs):** + +```tsx +// app/(tabs)/_layout.tsx +import { NativeTabs } from 'expo-router/unstable-native-tabs' + +export default function TabLayout() { + return ( + + + Home + + + + Settings + + + + ) +} +``` + +On iOS, native tabs automatically enable `contentInsetAdjustmentBehavior` on the +first `ScrollView` at the root of each tab screen, so content scrolls correctly +behind the translucent tab bar. If you need to disable this, use +`disableAutomaticContentInsets` on the trigger. + +### Prefer Native Header Options Over Custom Components + +**Incorrect (custom header component):** + +```tsx + , + }} +/> +``` + +**Correct (native header options):** + +```tsx + +``` + +Native headers support iOS large titles, search bars, blur effects, and proper +safe area handling automatically. + +### Why Native Navigators + +- **Performance**: Native transitions and gestures run on the UI thread +- **Platform behavior**: Automatic iOS large titles, Android material design +- **System integration**: Scroll-to-top on tab tap, PiP avoidance, proper safe + areas +- **Accessibility**: Platform accessibility features work automatically + +Reference: + +- [React Navigation Native Stack](https://reactnavigation.org/docs/native-stack-navigator) +- [React Native Bottom Tabs with React Navigation](https://oss.callstack.com/react-native-bottom-tabs/docs/guides/usage-with-react-navigation) +- [React Native Bottom Tabs with Expo Router](https://oss.callstack.com/react-native-bottom-tabs/docs/guides/usage-with-expo-router) +- [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 -} -``` - -**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 -} -``` - -### 8.2 Use .get() and .set() for Reanimated Shared Values (not .value) - -**Impact: LOW (required for React Compiler compatibility)** - -With React Compiler enabled, use `.get()` and `.set()` instead of reading or - -writing `.value` directly on Reanimated shared values. The compiler can't track - -property access—explicit methods ensure correct behavior. - -**Incorrect: breaks with React Compiler** - -```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 - + ``` -**Correct: compound components** - -```tsx -import { Pressable, Text } from 'react-native' - -function Button({ children }: { children: React.ReactNode }) { - return {children} -} +### 9.2 Styling with NativeWind and Modern Patterns -function ButtonText({ children }: { children: React.ReactNode }) { - return {children} -} +**Impact: MEDIUM (consistent design, cleaner layouts)** -function ButtonIcon({ children }: { children: React.ReactNode }) { - return <>{children} -} +This project uses NativeWind (Tailwind CSS for React Native). -// Usage is explicit and composable - +**Basic usage:** - +```tsx + + {title} + ``` ---- - -## 11. Monorepo +**Use gap for spacing:** -**Impact: LOW** - -Dependency management and native module configuration in -monorepos. +```tsx + + Title + Subtitle + +``` -### 11.1 Install Native Dependencies in App Directory +**Platform-specific styles:** -**Impact: CRITICAL (required for autolinking to work)** +```tsx + +``` -In a monorepo, packages with native code must be installed in the native app's +**Dark mode:** -directory directly. Autolinking only scans the app's `node_modules`—it won't +```tsx + + Content + +``` -find native dependencies installed in other packages. +### 9.3 Use Pressable Instead of Touchable Components -**Incorrect: native dep in shared package only** +**Impact: LOW (modern API, more flexible)** -```typescript -packages/ - ui/ - package.json # has react-native-reanimated - app/ - package.json # missing react-native-reanimated -``` +Never use `TouchableOpacity` or `TouchableHighlight`. Use `Pressable` instead. -Autolinking fails—native code not linked. +**Correct:** -**Correct: native dep in app directory** +```tsx +import { Pressable } from 'react-native' -```json -// packages/app/package.json -{ - "dependencies": { - "react-native-reanimated": "3.16.1" - } -} + + Press me + ``` -Even if the shared package uses the native dependency, the app must also list it - -for autolinking to detect and link the native code. - -### 11.2 Use Single Dependency Versions Across Monorepo +### 9.4 Use contentInsetAdjustmentBehavior for Safe Areas -**Impact: MEDIUM (avoids duplicate bundles, version conflicts)** +**Impact: MEDIUM (native safe area handling)** -Use a single version of each dependency across all packages in your monorepo. +Use `contentInsetAdjustmentBehavior="automatic"` on ScrollView instead of SafeAreaView. -Prefer exact versions over ranges. Multiple versions cause duplicate code in +**Correct:** -bundles, runtime conflicts, and inconsistent behavior across packages. +```tsx + + + Content + + +``` -Use a tool like syncpack to enforce this. As a last resort, use yarn resolutions +### 9.5 Use contentInset for Dynamic ScrollView Spacing -or npm overrides. +**Impact: LOW (smoother updates, no layout recalculation)** -**Incorrect: version ranges, multiple versions** +Use `contentInset` instead of padding for dynamic spacing. -```json -// packages/app/package.json -{ - "dependencies": { - "react-native-reanimated": "^3.0.0" - } -} +**Correct:** -// packages/ui/package.json -{ - "dependencies": { - "react-native-reanimated": "^3.5.0" - } -} +```tsx + + {children} + ``` -**Correct: exact versions, single source of truth** +### 9.6 Use Native Menus for Dropdowns and Context Menus -```json -// package.json (root) -{ - "pnpm": { - "overrides": { - "react-native-reanimated": "3.16.1" - } - } -} +**Impact: HIGH (native accessibility, platform-consistent UX)** -// packages/app/package.json -{ - "dependencies": { - "react-native-reanimated": "3.16.1" - } -} +Use native platform menus instead of custom JS implementations. -// packages/ui/package.json -{ - "dependencies": { - "react-native-reanimated": "3.16.1" - } -} -``` +**Correct (with zeego):** -Use your package manager's override/resolution feature to enforce versions at +```tsx +import * as DropdownMenu from 'zeego/dropdown-menu' -the root. When adding dependencies, specify exact versions without `^` or `~`. + + + Open Menu + + + console.log('edit')}> + Edit + + + +``` ---- +### 9.7 Use Native Modals Over JS-Based Bottom Sheets -## 12. Third-Party Dependencies +**Impact: HIGH (native performance, gestures, accessibility)** -**Impact: LOW** +Use native `` with `presentationStyle="formSheet"` instead of JS-based bottom sheets. -Wrapping and re-exporting third-party dependencies for -maintainability. +**Correct:** -### 12.1 Import from Design System Folder +```tsx + setVisible(false)} +> + Sheet content + +``` -**Impact: LOW (enables global changes and easy refactoring)** +### 9.8 Measuring View Dimensions -Re-export dependencies from a design system folder. App code imports from there, +**Impact: MEDIUM (synchronous measurement)** -not directly from packages. This enables global changes and easy refactoring. +Use both `useLayoutEffect` and `onLayout` for measuring views. -**Incorrect: imports directly from package** +**Correct:** ```tsx -import { View, Text } from 'react-native' -import { Button } from '@ui/button' - -function Profile() { - return ( - - Hello - - - ) -} -``` - -**Correct: imports from design system** +const ref = useRef(null) +const [size, setSize] = useState(undefined) -```tsx -import { View } from '@/components/view' -import { Text } from '@/components/text' -import { Button } from '@/components/button' +useLayoutEffect(() => { + const rect = ref.current?.getBoundingClientRect() + if (rect) setSize({ width: rect.width, height: rect.height }) +}, []) -function Profile() { - return ( - - Hello - - - ) +const onLayout = (e: LayoutChangeEvent) => { + const { width, height } = e.nativeEvent.layout + setSize((prev) => { + if (prev?.width === width && prev?.height === height) return prev + return { width, height } + }) } -``` -Start by simply re-exporting. Customize later without changing app code. +return {children} +``` --- -## 13. JavaScript +## 10. JavaScript **Impact: LOW** Micro-optimizations like hoisting expensive object creation. -### 13.1 Hoist Intl Formatter Creation +### 10.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. +Don't create `Intl.DateTimeFormat` or `Intl.NumberFormat` inside render. Hoist to module scope. -**Incorrect: new formatter every render** +**Incorrect:** ```tsx function Price({ amount }: { amount: number }) { - const formatter = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', - }) + const formatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }) return {formatter.format(amount)} } ``` -**Correct: hoisted to module scope** +**Correct:** ```tsx -const currencyFormatter = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', -}) +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. - --- -## 14. Fonts +## 11. Fonts **Impact: LOW** Native font loading for improved performance. -### 14.1 Load fonts natively at build time +### 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 instead of - -`useFonts` or `Font.loadAsync`. Embedded fonts are more efficient. - -[Expo Font Documentation](https://docs.expo.dev/versions/latest/sdk/font/) - -**Incorrect: async font loading** - -```tsx -import { useFonts } from 'expo-font' -import { Text, View } from 'react-native' +Use the `expo-font` config plugin to embed fonts at build time. -function App() { - const [fontsLoaded] = useFonts({ - 'Geist-Bold': require('./assets/fonts/Geist-Bold.otf'), - }) +**Correct (app.json):** - if (!fontsLoaded) { - return null +```json +{ + "expo": { + "plugins": [ + ["expo-font", { "fonts": ["./assets/fonts/Geist-Bold.otf"] }] + ] } - - return ( - - Hello - - ) } ``` -**Correct: config plugin, fonts embedded at build** - ```tsx -import { Text, View } from 'react-native' - -function App() { - // No loading state needed—font is already available - return ( - - Hello - - ) -} +// No loading state needed—font is already available +Hello ``` -After adding fonts to the config plugin, run `npx expo prebuild` and rebuild the - -native app. +After adding fonts, run `npx expo prebuild` and rebuild the native app. --- ## References -1. [https://react.dev](https://react.dev) -2. [https://reactnative.dev](https://reactnative.dev) -3. [https://docs.swmansion.com/react-native-reanimated](https://docs.swmansion.com/react-native-reanimated) -4. [https://docs.swmansion.com/react-native-gesture-handler](https://docs.swmansion.com/react-native-gesture-handler) -5. [https://docs.expo.dev](https://docs.expo.dev) -6. [https://legendapp.com/open-source/legend-list](https://legendapp.com/open-source/legend-list) -7. [https://github.com/nandorojo/galeria](https://github.com/nandorojo/galeria) -8. [https://zeego.dev](https://zeego.dev) +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 index 854db9f..83121c1 100644 --- a/.github/skills/react-native-skills/README.md +++ b/.github/skills/react-native-skills/README.md @@ -1,18 +1,28 @@ -# React Native Guidelines +# React Native & Expo Skills -A structured repository for creating and maintaining React Native Best Practices -optimized for agents and LLMs. +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 - - `area-description.md` - Individual rule files + - `{prefix}-{description}.md` - Individual rule files - `metadata.json` - Document metadata (version, organization, abstract) -- **`AGENTS.md`** - Compiled output (generated) +- `SKILL.md` - Skill overview and quick reference +- **`AGENTS.md`** - Compiled output for agents (generated) -## Rules +## Rules by Category ### Core Rendering (CRITICAL) @@ -21,8 +31,7 @@ optimized for agents and LLMs. ### List Performance (HIGH) -- `list-performance-virtualize.md` - Use virtualized lists (LegendList, - FlashList) +- `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 @@ -33,10 +42,9 @@ optimized for agents and LLMs. ### Animation (HIGH) -- `animation-gpu-properties.md` - Animate transform/opacity instead of layout +- `animation-gpu-properties.md` - Animate transform/opacity only - `animation-gesture-detector-press.md` - Use GestureDetector for press - animations -- `animation-derived-value.md` - Prefer useDerivedValue over useAnimatedReaction +- `animation-derived-value.md` - Prefer useDerivedValue ### Scroll Performance (HIGH) @@ -44,13 +52,13 @@ optimized for agents and LLMs. ### Navigation (HIGH) -- `navigation-native-navigators.md` - Use native stack and native tabs +- `navigation-native-navigators.md` - Use Expo Router ### React State (MEDIUM) - `react-state-dispatcher.md` - Use functional setState updates -- `react-state-fallback.md` - State should represent user intent only -- `react-state-minimize.md` - Minimize state variables, derive values +- `react-state-fallback.md` - State represents user intent only +- `react-state-minimize.md` - Minimize state, derive values ### State Architecture (MEDIUM) @@ -59,33 +67,18 @@ optimized for agents and LLMs. ### React Compiler (MEDIUM) - `react-compiler-destructure-functions.md` - Destructure functions early -- `react-compiler-reanimated-shared-values.md` - Use .get()/.set() for shared - values +- `react-compiler-reanimated-shared-values.md` - Use .get()/.set() ### User Interface (MEDIUM) -- `ui-expo-image.md` - Use expo-image for optimized images -- `ui-image-gallery.md` - Use Galeria for lightbox/galleries -- `ui-menus.md` - Native dropdown and context menus with Zeego +- `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 instead of TouchableOpacity +- `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 dynamic spacing -- `ui-styling.md` - Modern styling patterns (gap, boxShadow, gradients) - -### Design System (MEDIUM) - -- `design-system-compound-components.md` - Use compound components - -### Monorepo (LOW) - -- `monorepo-native-deps-in-app.md` - Install native deps in app directory -- `monorepo-single-dependency-versions.md` - Single dependency versions - -### Third-Party Dependencies (LOW) - -- `imports-design-system-folder.md` - Import from design system folder +- `ui-scrollview-content-inset.md` - Use contentInset for spacing +- `ui-styling.md` - NativeWind styling patterns ### JavaScript (LOW) @@ -97,30 +90,14 @@ optimized for agents and LLMs. ## Creating a New Rule -1. Copy `rules/_template.md` to `rules/area-description.md` -2. Choose the appropriate area prefix: - - `rendering-` for Core Rendering - - `list-performance-` for List Performance - - `animation-` for Animation - - `scroll-` for Scroll Performance - - `navigation-` for Navigation - - `react-state-` for React State - - `state-` for State Architecture - - `react-compiler-` for React Compiler - - `ui-` for User Interface - - `design-system-` for Design System - - `monorepo-` for Monorepo - - `imports-` for Third-Party Dependencies - - `js-` for JavaScript - - `fonts-` for Fonts +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. Ensure you have clear examples with explanations +4. Include clear incorrect/correct examples ## Rule File Structure -Each rule file should follow this structure: - -````markdown +```markdown --- title: Rule Title Here impact: MEDIUM @@ -132,34 +109,24 @@ tags: tag1, tag2, tag3 Brief explanation of the rule and why it matters. -**Incorrect (description of what's wrong):** +**Incorrect (description):** -```tsx +\`\`\`tsx // Bad code example -``` -```` +\`\`\` -**Correct (description of what's right):** +**Correct (description):** -```tsx +\`\`\`tsx // Good code example -``` +\`\`\` Reference: [Link](https://example.com) - ``` -## File Naming Convention - -- Files starting with `_` are special (excluded from build) -- Rule files: `area-description.md` (e.g., `animation-gpu-properties.md`) -- Section is automatically inferred from filename prefix -- Rules are sorted alphabetically by title within each section - ## Impact Levels -- `CRITICAL` - Highest priority, causes crashes or broken UI +- `CRITICAL` - Causes crashes or broken UI - `HIGH` - Significant performance improvements -- `MEDIUM` - Moderate 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 index 7340186..5043c19 100644 --- a/.github/skills/react-native-skills/SKILL.md +++ b/.github/skills/react-native-skills/SKILL.md @@ -1,105 +1,125 @@ --- -name: vercel-react-native-skills +name: react-native-expo-skills description: - React Native and Expo best practices for building performant mobile apps. Use - when building React Native components, optimizing list performance, - implementing animations, or working with native modules. Triggers on tasks - involving React Native, Expo, mobile performance, or native platform APIs. + 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: vercel - version: '1.0.0' + author: BitSleuth + version: '2.0.0' --- -# React Native Skills +# React Native & Expo Skills -Comprehensive best practices for React Native and Expo applications. Contains -rules across multiple categories covering performance, animations, UI patterns, +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 +- Building React Native or Expo apps for iOS/Android - Optimizing list and scroll performance - Implementing animations with Reanimated -- Working with images and media -- Configuring native modules or fonts -- Structuring monorepo projects with native dependencies +- Configuring navigation with Expo Router +- Styling components with NativeWind +- Working with images using expo-image ## Rule Categories by Priority -| Priority | Category | Impact | Prefix | -| -------- | ---------------- | -------- | -------------------- | -| 1 | List Performance | CRITICAL | `list-performance-` | -| 2 | Animation | HIGH | `animation-` | -| 3 | Navigation | HIGH | `navigation-` | -| 4 | UI Patterns | HIGH | `ui-` | -| 5 | State Management | MEDIUM | `react-state-` | -| 6 | Rendering | MEDIUM | `rendering-` | -| 7 | Monorepo | MEDIUM | `monorepo-` | -| 8 | Configuration | LOW | `fonts-`, `imports-` | +| 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. List Performance (CRITICAL) +### 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` - Memoize list item components -- `list-performance-callbacks` - Stabilize callback references +- `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` - Extract functions outside render -- `list-performance-images` - Optimize images in lists -- `list-performance-item-expensive` - Move expensive work outside items +- `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 -### 2. Animation (HIGH) +### 3. Animation (HIGH) -- `animation-gpu-properties` - Animate only transform and opacity +- `animation-gpu-properties` - Animate transform and opacity only - `animation-derived-value` - Use useDerivedValue for computed animations -- `animation-gesture-detector-press` - Use Gesture.Tap instead of Pressable +- `animation-gesture-detector-press` - Use Gesture.Tap for press animations -### 3. Navigation (HIGH) +### 4. Scroll Performance (HIGH) -- `navigation-native-navigators` - Use native stack and native tabs over JS navigators +- `scroll-position-no-state` - Never track scroll position in useState -### 4. UI Patterns (HIGH) +### 5. Navigation (HIGH) -- `ui-expo-image` - Use expo-image for all images -- `ui-image-gallery` - Use Galeria for image lightboxes -- `ui-pressable` - Use Pressable over TouchableOpacity -- `ui-safe-area-scroll` - Handle safe areas in ScrollViews -- `ui-scrollview-content-inset` - Use contentInset for headers -- `ui-menus` - Use native context menus -- `ui-native-modals` - Use native modals when possible -- `ui-measure-views` - Use onLayout, not measure() -- `ui-styling` - Use StyleSheet.create or Nativewind +- `navigation-native-navigators` - Use Expo Router for file-based navigation -### 5. State Management (MEDIUM) +### 6. React State (MEDIUM) -- `react-state-minimize` - Minimize state subscriptions -- `react-state-dispatcher` - Use dispatcher pattern for callbacks -- `react-state-fallback` - Show fallback on first render -- `react-compiler-destructure-functions` - Destructure for React Compiler -- `react-compiler-reanimated-shared-values` - Handle shared values with compiler +- `react-state-minimize` - Minimize state variables, derive values +- `react-state-dispatcher` - Use functional setState updates +- `react-state-fallback` - Use fallback state instead of initialState -### 6. Rendering (MEDIUM) +### 7. State Architecture (MEDIUM) -- `rendering-text-in-text-component` - Wrap text in Text components -- `rendering-no-falsy-and` - Avoid falsy && for conditional rendering +- `state-ground-truth` - State must represent ground truth -### 7. Monorepo (MEDIUM) +### 8. React Compiler (MEDIUM) -- `monorepo-native-deps-in-app` - Keep native dependencies in app package -- `monorepo-single-dependency-versions` - Use single versions across packages +- `react-compiler-destructure-functions` - Destructure functions early +- `react-compiler-reanimated-shared-values` - Use .get()/.set() for shared values -### 8. Configuration (LOW) +### 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) -- `fonts-config-plugin` - Use config plugins for custom fonts -- `imports-design-system-folder` - Organize design system imports - `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: @@ -107,6 +127,7 @@ 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: diff --git a/.github/skills/react-native-skills/metadata.json b/.github/skills/react-native-skills/metadata.json index 600eb5b..37ab176 100644 --- a/.github/skills/react-native-skills/metadata.json +++ b/.github/skills/react-native-skills/metadata.json @@ -1,16 +1,14 @@ { - "version": "1.0.0", - "organization": "Engineering", + "version": "2.0.0", + "organization": "BitSleuth", "date": "January 2026", - "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.", + "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://docs.expo.dev", - "https://legendapp.com/open-source/legend-list", - "https://github.com/nandorojo/galeria", - "https://zeego.dev" + "https://www.nativewind.dev" ] } diff --git a/.github/skills/react-native-skills/rules/_sections.md b/.github/skills/react-native-skills/rules/_sections.md index 0519cf2..8fba312 100644 --- a/.github/skills/react-native-skills/rules/_sections.md +++ b/.github/skills/react-native-skills/rules/_sections.md @@ -7,80 +7,62 @@ The section ID (in parentheses) is the filename prefix used to group rules. ## 1. Core Rendering (rendering) -**Impact:** CRITICAL +**Impact:** CRITICAL **Description:** Fundamental React Native rendering rules. Violations cause runtime crashes or broken UI. ## 2. List Performance (list-performance) -**Impact:** HIGH -**Description:** Optimizing virtualized lists (FlatList, LegendList, FlashList) -for smooth scrolling and fast updates. +**Impact:** HIGH +**Description:** Optimizing virtualized lists (FlatList, FlashList) for smooth +scrolling and fast updates. ## 3. Animation (animation) -**Impact:** HIGH +**Impact:** HIGH **Description:** GPU-accelerated animations, Reanimated patterns, and avoiding render thrashing during gestures. ## 4. Scroll Performance (scroll) -**Impact:** HIGH +**Impact:** HIGH **Description:** Tracking scroll position without causing render thrashing. ## 5. Navigation (navigation) -**Impact:** HIGH -**Description:** Using native navigators for stack and tab navigation instead of -JS-based alternatives. +**Impact:** HIGH +**Description:** Using Expo Router and native navigators for file-based routing +on iOS and Android. ## 6. React State (react-state) -**Impact:** MEDIUM +**Impact:** MEDIUM **Description:** Patterns for managing React state to avoid stale closures and unnecessary re-renders. ## 7. State Architecture (state) -**Impact:** MEDIUM +**Impact:** MEDIUM **Description:** Ground truth principles for state variables and derived values. ## 8. React Compiler (react-compiler) -**Impact:** MEDIUM +**Impact:** MEDIUM **Description:** Compatibility patterns for React Compiler with React Native and Reanimated. ## 9. User Interface (ui) -**Impact:** MEDIUM -**Description:** Native UI patterns for images, menus, modals, styling, and -platform-consistent interfaces. +**Impact:** MEDIUM +**Description:** Native UI patterns for images, menus, modals, styling with +NativeWind, and platform-consistent interfaces. -## 10. Design System (design-system) +## 10. JavaScript (js) -**Impact:** MEDIUM -**Description:** Architecture patterns for building maintainable component -libraries. - -## 11. Monorepo (monorepo) - -**Impact:** LOW -**Description:** Dependency management and native module configuration in -monorepos. - -## 12. Third-Party Dependencies (imports) - -**Impact:** LOW -**Description:** Wrapping and re-exporting third-party dependencies for -maintainability. - -## 13. JavaScript (js) - -**Impact:** LOW +**Impact:** LOW **Description:** Micro-optimizations like hoisting expensive object creation. -## 14. Fonts (fonts) +## 11. Fonts (fonts) -**Impact:** LOW +**Impact:** LOW **Description:** Native font loading for improved performance. diff --git a/.github/skills/react-native-skills/rules/design-system-compound-components.md b/.github/skills/react-native-skills/rules/design-system-compound-components.md deleted file mode 100644 index d8239ee..0000000 --- a/.github/skills/react-native-skills/rules/design-system-compound-components.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Use Compound Components Over Polymorphic Children -impact: MEDIUM -impactDescription: flexible composition, clearer API -tags: design-system, components, composition ---- - -## Use Compound Components Over Polymorphic Children - -Don't create components that can accept a string if they aren't a text node. If -a component can receive a string child, it must be a dedicated `*Text` -component. For components like buttons, which can have both a View (or -Pressable) together with text, use compound components, such a `Button`, -`ButtonText`, and `ButtonIcon`. - -**Incorrect (polymorphic children):** - -```tsx -import { Pressable, Text } from 'react-native' - -type ButtonProps = { - children: string | React.ReactNode - icon?: React.ReactNode -} - -function Button({ children, icon }: ButtonProps) { - return ( - - {icon} - {typeof children === 'string' ? {children} : children} - - ) -} - -// Usage is ambiguous - - -``` - -**Correct (compound components):** - -```tsx -import { Pressable, Text } from 'react-native' - -function Button({ children }: { children: React.ReactNode }) { - return {children} -} - -function ButtonText({ children }: { children: React.ReactNode }) { - return {children} -} - -function ButtonIcon({ children }: { children: React.ReactNode }) { - return <>{children} -} - -// Usage is explicit and composable - - - -``` diff --git a/.github/skills/react-native-skills/rules/imports-design-system-folder.md b/.github/skills/react-native-skills/rules/imports-design-system-folder.md deleted file mode 100644 index 8466dcb..0000000 --- a/.github/skills/react-native-skills/rules/imports-design-system-folder.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: Import from Design System Folder -impact: LOW -impactDescription: enables global changes and easy refactoring -tags: imports, architecture, design-system ---- - -## Import from Design System Folder - -Re-export dependencies from a design system folder. App code imports from there, -not directly from packages. This enables global changes and easy refactoring. - -**Incorrect (imports directly from package):** - -```tsx -import { View, Text } from 'react-native' -import { Button } from '@ui/button' - -function Profile() { - return ( - - Hello - - - ) -} -``` - -**Correct (imports from design system):** - -```tsx -// components/view.tsx -import { View as RNView } from 'react-native' - -// ideal: pick the props you will actually use to control implementation -export function View( - props: Pick, 'style' | 'children'> -) { - return -} -``` - -```tsx -// components/text.tsx -export { Text } from 'react-native' -``` - -```tsx -// components/button.tsx -export { Button } from '@ui/button' -``` - -```tsx -import { View } from '@/components/view' -import { Text } from '@/components/text' -import { Button } from '@/components/button' - -function Profile() { - return ( - - Hello - - - ) -} -``` - -Start by simply re-exporting. Customize later without changing app code. diff --git a/.github/skills/react-native-skills/rules/monorepo-native-deps-in-app.md b/.github/skills/react-native-skills/rules/monorepo-native-deps-in-app.md deleted file mode 100644 index ff85d76..0000000 --- a/.github/skills/react-native-skills/rules/monorepo-native-deps-in-app.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: Install Native Dependencies in App Directory -impact: CRITICAL -impactDescription: required for autolinking to work -tags: monorepo, native, autolinking, installation ---- - -## Install Native Dependencies in App Directory - -In a monorepo, packages with native code must be installed in the native app's -directory directly. Autolinking only scans the app's `node_modules`—it won't -find native dependencies installed in other packages. - -**Incorrect (native dep in shared package only):** - -``` -packages/ - ui/ - package.json # has react-native-reanimated - app/ - package.json # missing react-native-reanimated -``` - -Autolinking fails—native code not linked. - -**Correct (native dep in app directory):** - -``` -packages/ - ui/ - package.json # has react-native-reanimated - app/ - package.json # also has react-native-reanimated -``` - -```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. diff --git a/.github/skills/react-native-skills/rules/monorepo-single-dependency-versions.md b/.github/skills/react-native-skills/rules/monorepo-single-dependency-versions.md deleted file mode 100644 index 1087dfa..0000000 --- a/.github/skills/react-native-skills/rules/monorepo-single-dependency-versions.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: Use Single Dependency Versions Across Monorepo -impact: MEDIUM -impactDescription: avoids duplicate bundles, version conflicts -tags: monorepo, dependencies, installation ---- - -## Use Single Dependency Versions Across Monorepo - -Use a single version of each dependency across all packages in your monorepo. -Prefer exact versions over ranges. Multiple versions cause duplicate code in -bundles, runtime conflicts, and inconsistent behavior across packages. - -Use a tool like syncpack to enforce this. As a last resort, use yarn resolutions -or npm overrides. - -**Incorrect (version ranges, multiple versions):** - -```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 `~`. diff --git a/.github/skills/react-native-skills/rules/navigation-native-navigators.md b/.github/skills/react-native-skills/rules/navigation-native-navigators.md index 035c5fd..ad60a54 100644 --- a/.github/skills/react-native-skills/rules/navigation-native-navigators.md +++ b/.github/skills/react-native-skills/rules/navigation-native-navigators.md @@ -1,154 +1,101 @@ --- -title: Use Native Navigators for Navigation +title: Use Expo Router for File-Based Navigation impact: HIGH impactDescription: native performance, platform-appropriate UI -tags: navigation, react-navigation, expo-router, native-stack, tabs +tags: navigation, expo-router, native-stack, tabs, ios, android --- -## Use Native Navigators for Navigation +## Use Expo Router for File-Based Navigation -Always use native navigators instead of JS-based ones. Native navigators use -platform APIs (UINavigationController on iOS, Fragment on Android) for better +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. -**For stacks:** Use `@react-navigation/native-stack` or expo-router's default -stack (which uses native-stack). Avoid `@react-navigation/stack`. - -**For tabs:** Use `react-native-bottom-tabs` (native) or expo-router's native -tabs. Avoid `@react-navigation/bottom-tabs` when native feel matters. - ### Stack Navigation -**Incorrect (JS stack navigator):** - -```tsx -import { createStackNavigator } from '@react-navigation/stack' - -const Stack = createStackNavigator() - -function App() { - return ( - - - - - ) -} -``` - -**Correct (native stack with react-navigation):** - -```tsx -import { createNativeStackNavigator } from '@react-navigation/native-stack' - -const Stack = createNativeStackNavigator() - -function App() { - return ( - - - - - ) -} -``` - -**Correct (expo-router uses native stack by default):** +Expo Router uses native stack by default: ```tsx // app/_layout.tsx import { Stack } from 'expo-router' -export default function Layout() { - return -} -``` - -### Tab Navigation - -**Incorrect (JS bottom tabs):** - -```tsx -import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' - -const Tab = createBottomTabNavigator() - -function App() { +export default function RootLayout() { return ( - - - - + + + + ) } ``` -**Correct (native bottom tabs with react-navigation):** +### Tab Navigation + +Use the Tabs component for bottom tab navigation: ```tsx -import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation' - -const Tab = createNativeBottomTabNavigator() +// app/(tabs)/_layout.tsx +import { Tabs } from 'expo-router' -function App() { +export default function TabLayout() { return ( - - + ({ sfSymbol: 'house' }), + title: 'Home', + tabBarIcon: ({ color }) => , }} /> - ({ sfSymbol: 'gear' }), + title: 'Send', + tabBarIcon: ({ color }) => , }} /> - + , + }} + /> + , + }} + /> + ) } ``` -**Correct (expo-router native tabs):** +### Modal Screens -```tsx -// app/(tabs)/_layout.tsx -import { NativeTabs } from 'expo-router/unstable-native-tabs' +Use presentation: 'modal' for modal screens: -export default function TabLayout() { - return ( - - - Home - - - - Settings - - - - ) -} +```tsx + ``` -On iOS, native tabs automatically enable `contentInsetAdjustmentBehavior` on the -first `ScrollView` at the root of each tab screen, so content scrolls correctly -behind the translucent tab bar. If you need to disable this, use -`disableAutomaticContentInsets` on the trigger. - ### Prefer Native Header Options Over Custom Components **Incorrect (custom header component):** ```tsx , + header: () => , }} /> ``` @@ -157,11 +104,10 @@ behind the translucent tab bar. If you need to disable this, use ```tsx (null) - - return ( - <> - {urls.map((url) => ( - setSelected(url)}> - - - ))} - setSelected(null)}> - - - - ) -} -``` - -**Correct (Galeria with expo-image):** - -```tsx -import { Galeria } from '@nandorojo/galeria' -import { Image } from 'expo-image' - -function ImageGallery({ urls }: { urls: string[] }) { - return ( - - {urls.map((url, index) => ( - - - - ))} - - ) -} -``` - -**Single image:** - -```tsx -import { Galeria } from '@nandorojo/galeria' -import { Image } from 'expo-image' - -function Avatar({ url }: { url: string }) { - return ( - - - - - - ) -} -``` - -**With low-res thumbnails and high-res fullscreen:** - -```tsx - - {lowResUrls.map((url, index) => ( - - - - ))} - -``` - -**With FlashList:** - -```tsx - - ( - - - - )} - numColumns={3} - estimatedItemSize={100} - /> - -``` - -Works with `expo-image`, `SolitoImage`, `react-native` Image, or any image -component. - -Reference: [Galeria](https://github.com/nandorojo/galeria) diff --git a/.github/skills/react-native-skills/rules/ui-styling.md b/.github/skills/react-native-skills/rules/ui-styling.md index 3908de3..a3b9946 100644 --- a/.github/skills/react-native-skills/rules/ui-styling.md +++ b/.github/skills/react-native-skills/rules/ui-styling.md @@ -1,87 +1,180 @@ --- -title: Modern React Native Styling Patterns +title: Styling with NativeWind and Modern Patterns impact: MEDIUM -impactDescription: consistent design, smoother borders, cleaner layouts -tags: styling, css, layout, shadows, gradients +impactDescription: consistent design, cleaner layouts, better DX +tags: styling, nativewind, tailwind, css, layout, shadows --- -## Modern React Native Styling Patterns +## Styling with NativeWind and Modern Patterns -Follow these styling patterns for cleaner, more consistent React Native code. +This project uses NativeWind (Tailwind CSS for React Native). Follow these +patterns for consistent, maintainable styling. -**Always use `borderCurve: 'continuous'` with `borderRadius`:** +### NativeWind Basics + +Use Tailwind classes via the `className` prop: ```tsx -// Incorrect -{ borderRadius: 12 } +import { View, Text } from 'react-native' + +function Card({ title, children }: Props) { + return ( + + {title} + {children} + + ) +} +``` -// Correct – smoother iOS-style corners -{ borderRadius: 12, borderCurve: 'continuous' } +### Prefer NativeWind Over StyleSheet + +**Incorrect (verbose StyleSheet):** + +```tsx +import { View, Text, StyleSheet } from 'react-native' + +function Card({ title }: Props) { + return ( + + {title} + + ) +} + +const styles = StyleSheet.create({ + container: { + backgroundColor: '#fff', + borderRadius: 12, + padding: 16, + }, + title: { + fontSize: 18, + fontWeight: '600', + color: '#111', + }, +}) +``` + +**Correct (concise NativeWind):** + +```tsx +import { View, Text } from 'react-native' + +function Card({ title }: Props) { + return ( + + {title} + + ) +} ``` -**Use `gap` instead of margin for spacing between elements:** +### Use gap for Spacing + +**Incorrect (margin on children):** ```tsx -// Incorrect – margin on children - Title - Subtitle + Title + Subtitle +``` + +**Correct (gap on parent):** -// Correct – gap on parent - +```tsx + Title Subtitle ``` -**Use `padding` for space within, `gap` for space between:** +### Combining Static and Dynamic Styles + +For conditional styling, combine NativeWind classes with template literals: + +```tsx +function Button({ variant, disabled }: Props) { + return ( + + + Press me + + + ) +} +``` + +### Platform-Specific Styles + +Use platform prefixes for platform-specific styles: ```tsx - - First - Second + + Content ``` -**Use `experimental_backgroundImage` for linear gradients:** +### Dark Mode Support + +Use dark mode variants: ```tsx -// Incorrect – third-party gradient library - - -// Correct – native CSS gradient syntax - + + Content + ``` -**Use CSS `boxShadow` string syntax for shadows:** +### Modern React Native Style Properties + +When using inline styles (for animations or dynamic values), use modern patterns: + +**Use `borderCurve: 'continuous'` with borderRadius:** ```tsx -// Incorrect – legacy shadow objects or elevation -{ shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1 } -{ elevation: 4 } +// Smoother iOS-style corners +{ borderRadius: 12, borderCurve: 'continuous' } +``` + +**Use CSS boxShadow syntax:** -// Correct – CSS box-shadow syntax +```tsx +// Modern shadow syntax { boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' } ``` -**Avoid multiple font sizes – use weight and color for emphasis:** +**Use experimental_backgroundImage for gradients:** ```tsx -// Incorrect – varying font sizes for hierarchy -Title -Subtitle -Caption - -// Correct – consistent size, vary weight and color -Title -Subtitle -Caption +// Native gradient support +{ + experimental_backgroundImage: 'linear-gradient(to bottom, #000, #fff)' +} ``` -Limiting font sizes creates visual consistency. Use `fontWeight` (bold/semibold) -and grayscale colors for hierarchy instead. +### Common NativeWind Patterns + +**Flex layout:** +```tsx + +``` + +**Safe area padding:** +```tsx + +``` + +**Responsive spacing:** +```tsx + +``` + +Reference: + +- [NativeWind Documentation](https://www.nativewind.dev/) +- [Tailwind CSS](https://tailwindcss.com/docs)