

Also from Kynth Studios


Also from Kynth Studios


Also from Kynth Studios
1# React Native Skills23**Version 1.0.0**4Engineering5January 202667> **Note:**8> This document is mainly for agents and LLMs to follow when maintaining,9> generating, or refactoring React Native codebases. Humans10> may also find it useful, but guidance here is optimized for automation11> and consistency by AI-assisted workflows.1213---1415## Abstract1617Comprehensive 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.1819---2021## Table of Contents22231. [Core Rendering](#1-core-rendering) — **CRITICAL**24 - 1.1 [Never Use && with Potentially Falsy Values](#11-never-use--with-potentially-falsy-values)25 - 1.2 [Wrap Strings in Text Components](#12-wrap-strings-in-text-components)262. [List Performance](#2-list-performance) — **HIGH**27 - 2.1 [Avoid Inline Objects in renderItem](#21-avoid-inline-objects-in-renderitem)28 - 2.2 [Hoist callbacks to the root of lists](#22-hoist-callbacks-to-the-root-of-lists)29 - 2.3 [Keep List Items Lightweight](#23-keep-list-items-lightweight)30 - 2.4 [Optimize List Performance with Stable Object References](#24-optimize-list-performance-with-stable-object-references)31 - 2.5 [Pass Primitives to List Items for Memoization](#25-pass-primitives-to-list-items-for-memoization)32 - 2.6 [Use a List Virtualizer for Any List](#26-use-a-list-virtualizer-for-any-list)33 - 2.7 [Use Compressed Images in Lists](#27-use-compressed-images-in-lists)34 - 2.8 [Use Item Types for Heterogeneous Lists](#28-use-item-types-for-heterogeneous-lists)353. [Animation](#3-animation) — **HIGH**36 - 3.1 [Animate Transform and Opacity Instead of Layout Properties](#31-animate-transform-and-opacity-instead-of-layout-properties)37 - 3.2 [Prefer useDerivedValue Over useAnimatedReaction](#32-prefer-usederivedvalue-over-useanimatedreaction)38 - 3.3 [Use GestureDetector for Animated Press States](#33-use-gesturedetector-for-animated-press-states)394. [Scroll Performance](#4-scroll-performance) — **HIGH**40 - 4.1 [Never Track Scroll Position in useState](#41-never-track-scroll-position-in-usestate)415. [Navigation](#5-navigation) — **HIGH**42 - 5.1 [Use Native Navigators for Navigation](#51-use-native-navigators-for-navigation)436. [React State](#6-react-state) — **MEDIUM**44 - 6.1 [Minimize State Variables and Derive Values](#61-minimize-state-variables-and-derive-values)45 - 6.2 [Use fallback state instead of initialState](#62-use-fallback-state-instead-of-initialstate)46 - 6.3 [useState Dispatch updaters for State That Depends on Current Value](#63-usestate-dispatch-updaters-for-state-that-depends-on-current-value)477. [State Architecture](#7-state-architecture) — **MEDIUM**48 - 7.1 [State Must Represent Ground Truth](#71-state-must-represent-ground-truth)498. [React Compiler](#8-react-compiler) — **MEDIUM**50 - 8.1 [Destructure Functions Early in Render (React Compiler)](#81-destructure-functions-early-in-render-react-compiler)51 - 8.2 [Use .get() and .set() for Reanimated Shared Values (not .value)](#82-use-get-and-set-for-reanimated-shared-values-not-value)529. [User Interface](#9-user-interface) — **MEDIUM**53 - 9.1 [Measuring View Dimensions](#91-measuring-view-dimensions)54 - 9.2 [Modern React Native Styling Patterns](#92-modern-react-native-styling-patterns)55 - 9.3 [Use contentInset for Dynamic ScrollView Spacing](#93-use-contentinset-for-dynamic-scrollview-spacing)56 - 9.4 [Use contentInsetAdjustmentBehavior for Safe Areas](#94-use-contentinsetadjustmentbehavior-for-safe-areas)57 - 9.5 [Use expo-image for Optimized Images](#95-use-expo-image-for-optimized-images)58 - 9.6 [Use Galeria for Image Galleries and Lightbox](#96-use-galeria-for-image-galleries-and-lightbox)59 - 9.7 [Use Native Menus for Dropdowns and Context Menus](#97-use-native-menus-for-dropdowns-and-context-menus)60 - 9.8 [Use Native Modals Over JS-Based Bottom Sheets](#98-use-native-modals-over-js-based-bottom-sheets)61 - 9.9 [Use Pressable Instead of Touchable Components](#99-use-pressable-instead-of-touchable-components)6210. [Design System](#10-design-system) — **MEDIUM**63 - 10.1 [Use Compound Components Over Polymorphic Children](#101-use-compound-components-over-polymorphic-children)6411. [Monorepo](#11-monorepo) — **LOW**65 - 11.1 [Install Native Dependencies in App Directory](#111-install-native-dependencies-in-app-directory)66 - 11.2 [Use Single Dependency Versions Across Monorepo](#112-use-single-dependency-versions-across-monorepo)6712. [Third-Party Dependencies](#12-third-party-dependencies) — **LOW**68 - 12.1 [Import from Design System Folder](#121-import-from-design-system-folder)6913. [JavaScript](#13-javascript) — **LOW**70 - 13.1 [Hoist Intl Formatter Creation](#131-hoist-intl-formatter-creation)7114. [Fonts](#14-fonts) — **LOW**72 - 14.1 [Load fonts natively at build time](#141-load-fonts-natively-at-build-time)7374---7576## 1. Core Rendering7778**Impact: CRITICAL**7980Fundamental React Native rendering rules. Violations cause81runtime crashes or broken UI.8283### 1.1 Never Use && with Potentially Falsy Values8485**Impact: CRITICAL (prevents production crash)**8687Never use `{value && <Component />}` when `value` could be an empty string or8889`0`. These are falsy but JSX-renderable—React Native will try to render them as9091text outside a `<Text>` component, causing a hard crash in production.9293**Incorrect: crashes if count is 0 or name is ""**9495```tsx96function Profile({ name, count }: { name: string; count: number }) {97 return (98 <View>99 {name && <Text>{name}</Text>}100 {count && <Text>{count} items</Text>}101 </View>102 )103}104// If name="" or count=0, renders the falsy value → crash105```106107**Correct: ternary with null**108109```tsx110function Profile({ name, count }: { name: string; count: number }) {111 return (112 <View>113 {name ? <Text>{name}</Text> : null}114 {count ? <Text>{count} items</Text> : null}115 </View>116 )117}118```119120**Correct: explicit boolean coercion**121122```tsx123function Profile({ name, count }: { name: string; count: number }) {124 return (125 <View>126 {!!name && <Text>{name}</Text>}127 {!!count && <Text>{count} items</Text>}128 </View>129 )130}131```132133**Best: early return**134135```tsx136function Profile({ name, count }: { name: string; count: number }) {137 if (!name) return null138139 return (140 <View>141 <Text>{name}</Text>142 {count > 0 ? <Text>{count} items</Text> : null}143 </View>144 )145}146```147148Early returns are clearest. When using conditionals inline, prefer ternary or149150explicit boolean checks.151152**Lint rule:** Enable `react/jsx-no-leaked-render` from153154[eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-leaked-render.md)155156to catch this automatically.157158### 1.2 Wrap Strings in Text Components159160**Impact: CRITICAL (prevents runtime crash)**161162Strings must be rendered inside `<Text>`. React Native crashes if a string is a163164direct child of `<View>`.165166**Incorrect: crashes**167168```tsx169import { View } from 'react-native'170171function Greeting({ name }: { name: string }) {172 return <View>Hello, {name}!</View>173}174// Error: Text strings must be rendered within a <Text> component.175```176177**Correct:**178179```tsx180import { View, Text } from 'react-native'181182function Greeting({ name }: { name: string }) {183 return (184 <View>185 <Text>Hello, {name}!</Text>186 </View>187 )188}189```190191---192193## 2. List Performance194195**Impact: HIGH**196197Optimizing virtualized lists (FlatList, LegendList, FlashList)198for smooth scrolling and fast updates.199200### 2.1 Avoid Inline Objects in renderItem201202**Impact: HIGH (prevents unnecessary re-renders of memoized list items)**203204Don't create new objects inside `renderItem` to pass as props. Inline objects205206create new references on every render, breaking memoization. Pass primitive207208values directly from `item` instead.209210**Incorrect: inline object breaks memoization**211212```tsx213function UserList({ users }: { users: User[] }) {214 return (215 <LegendList216 data={users}217 renderItem={({ item }) => (218 <UserRow219 // Bad: new object on every render220 user={{ id: item.id, name: item.name, avatar: item.avatar }}221 />222 )}223 />224 )225}226```227228**Incorrect: inline style object**229230```tsx231renderItem={({ item }) => (232 <UserRow233 name={item.name}234 // Bad: new style object on every render235 style={{ backgroundColor: item.isActive ? 'green' : 'gray' }}236 />237)}238```239240**Correct: pass item directly or primitives**241242```tsx243function UserList({ users }: { users: User[] }) {244 return (245 <LegendList246 data={users}247 renderItem={({ item }) => (248 // Good: pass the item directly249 <UserRow user={item} />250 )}251 />252 )253}254```255256**Correct: pass primitives, derive inside child**257258```tsx259renderItem={({ item }) => (260 <UserRow261 id={item.id}262 name={item.name}263 isActive={item.isActive}264 />265)}266267const UserRow = memo(function UserRow({ id, name, isActive }: Props) {268 // Good: derive style inside memoized component269 const backgroundColor = isActive ? 'green' : 'gray'270 return <View style={[styles.row, { backgroundColor }]}>{/* ... */}</View>271})272```273274**Correct: hoist static styles in module scope**275276```tsx277const activeStyle = { backgroundColor: 'green' }278const inactiveStyle = { backgroundColor: 'gray' }279280renderItem={({ item }) => (281 <UserRow282 name={item.name}283 // Good: stable references284 style={item.isActive ? activeStyle : inactiveStyle}285 />286)}287```288289Passing primitives or stable references allows `memo()` to skip re-renders when290291the actual values haven't changed.292293**Note:** If you have the React Compiler enabled, it handles memoization294295automatically and these manual optimizations become less critical.296297### 2.2 Hoist callbacks to the root of lists298299**Impact: MEDIUM (Fewer re-renders and faster lists)**300301When passing callback functions to list items, create a single instance of the302303callback at the root of the list. Items should then call it with a unique304305identifier.306307**Incorrect: creates a new callback on each render**308309```typescript310return (311 <LegendList312 renderItem={({ item }) => {313 // bad: creates a new callback on each render314 const onPress = () => handlePress(item.id)315 return <Item key={item.id} item={item} onPress={onPress} />316 }}317 />318)319```320321**Correct: a single function instance passed to each item**322323```typescript324const onPress = useCallback(() => handlePress(item.id), [handlePress, item.id])325326return (327 <LegendList328 renderItem={({ item }) => (329 <Item key={item.id} item={item} onPress={onPress} />330 )}331 />332)333```334335Reference: [https://example.com](https://example.com)336337### 2.3 Keep List Items Lightweight338339**Impact: HIGH (reduces render time for visible items during scroll)**340341List items should be as inexpensive as possible to render. Minimize hooks, avoid342343queries, and limit React Context access. Virtualized lists render many items344345during scroll—expensive items cause jank.346347**Incorrect: heavy list item**348349```tsx350function ProductRow({ id }: { id: string }) {351 // Bad: query inside list item352 const { data: product } = useQuery(['product', id], () => fetchProduct(id))353 // Bad: multiple context accesses354 const theme = useContext(ThemeContext)355 const user = useContext(UserContext)356 const cart = useContext(CartContext)357 // Bad: expensive computation358 const recommendations = useMemo(359 () => computeRecommendations(product),360 [product]361 )362363 return <View>{/* ... */}</View>364}365```366367**Correct: lightweight list item**368369```tsx370function ProductRow({ name, price, imageUrl }: Props) {371 // Good: receives only primitives, minimal hooks372 return (373 <View>374 <Image source={{ uri: imageUrl }} />375 <Text>{name}</Text>376 <Text>{price}</Text>377 </View>378 )379}380```381382**Move data fetching to parent:**383384```tsx385// Parent fetches all data once386function ProductList() {387 const { data: products } = useQuery(['products'], fetchProducts)388389 return (390 <LegendList391 data={products}392 renderItem={({ item }) => (393 <ProductRow name={item.name} price={item.price} imageUrl={item.image} />394 )}395 />396 )397}398```399400**For shared values, use Zustand selectors instead of Context:**401402```tsx403// Incorrect: Context causes re-render when any cart value changes404function ProductRow({ id, name }: Props) {405 const { items } = useContext(CartContext)406 const inCart = items.includes(id)407 // ...408}409410// Correct: Zustand selector only re-renders when this specific value changes411function ProductRow({ id, name }: Props) {412 // use Set.has (created once at the root) instead of Array.includes()413 const inCart = useCartStore((s) => s.items.has(id))414 // ...415}416```417418**Guidelines for list items:**419420- No queries or data fetching421422- No expensive computations (move to parent or memoize at parent level)423424- Prefer Zustand selectors over React Context425426- Minimize useState/useEffect hooks427428- Pass pre-computed values as props429430The goal: list items should be simple rendering functions that take props and431432return JSX.433434### 2.4 Optimize List Performance with Stable Object References435436**Impact: CRITICAL (virtualization relies on reference stability)**437438Don't map or filter data before passing to virtualized lists. Virtualization439440relies on object reference stability to know what changed—new references cause441442full re-renders of all visible items. Attempt to prevent frequent renders at the443444list-parent level.445446Where needed, use context selectors within list items.447448**Incorrect: creates new object references on every keystroke**449450```tsx451function DomainSearch() {452 const { keyword, setKeyword } = useKeywordZustandState()453 const { data: tlds } = useTlds()454455 // Bad: creates new objects on every render, reparenting the entire list on every keystroke456 const domains = tlds.map((tld) => ({457 domain: `${keyword}.${tld.name}`,458 tld: tld.name,459 price: tld.price,460 }))461462 return (463 <>464 <TextInput value={keyword} onChangeText={setKeyword} />465 <LegendList466 data={domains}467 renderItem={({ item }) => <DomainItem item={item} keyword={keyword} />}468 />469 </>470 )471}472```473474**Correct: stable references, transform inside items**475476```tsx477const renderItem = ({ item }) => <DomainItem tld={item} />478479function DomainSearch() {480 const { data: tlds } = useTlds()481482 return (483 <LegendList484 // good: as long as the data is stable, LegendList will not re-render the entire list485 data={tlds}486 renderItem={renderItem}487 />488 )489}490491function DomainItem({ tld }: { tld: Tld }) {492 // good: transform within items, and don't pass the dynamic data as a prop493 // good: use a selector function from zustand to receive a stable string back494 const domain = useKeywordZustandState((s) => s.keyword + '.' + tld.name)495 return <Text>{domain}</Text>496}497```498499**Updating parent array reference:**500501```tsx502// good: creates a new array instance without mutating the inner objects503// good: parent array reference is unaffected by typing and updating "keyword"504const sortedTlds = tlds.toSorted((a, b) => a.name.localeCompare(b.name))505506return <LegendList data={sortedTlds} renderItem={renderItem} />507```508509Creating a new array instance can be okay, as long as its inner object510511references are stable. For instance, if you sort a list of objects:512513Even though this creates a new array instance `sortedTlds`, the inner object514515references are stable.516517**With zustand for dynamic data: avoids parent re-renders**518519```tsx520function DomainItemFavoriteButton({ tld }: { tld: Tld }) {521 const isFavorited = useFavoritesStore((s) => s.favorites.has(tld.id))522 return <TldFavoriteButton isFavorited={isFavorited} />523}524```525526Virtualization can now skip items that haven't changed when typing. Only visible527528items (~20) re-render on keystroke, rather than the parent.529530**Deriving state within list items based on parent data (avoids parent531532re-renders):**533534For components where the data is conditional based on the parent state, this535536pattern is even more important. For example, if you are checking if an item is537538favorited, toggling favorites only re-renders one component if the item itself539540is in charge of accessing the state rather than the parent:541542Note: if you're using the React Compiler, you can read React Context values543544directly within list items. Although this is slightly slower than using a545546Zustand selector in most cases, the effect may be negligible.547548### 2.5 Pass Primitives to List Items for Memoization549550**Impact: HIGH (enables effective memo() comparison)**551552When possible, pass only primitive values (strings, numbers, booleans) as props553554to list item components. Primitives enable shallow comparison in `memo()` to555556work correctly, skipping re-renders when values haven't changed.557558**Incorrect: object prop requires deep comparison**559560```tsx561type User = { id: string; name: string; email: string; avatar: string }562563const UserRow = memo(function UserRow({ user }: { user: User }) {564 // memo() compares user by reference, not value565 // If parent creates new user object, this re-renders even if data is same566 return <Text>{user.name}</Text>567})568569renderItem={({ item }) => <UserRow user={item} />}570```571572This can still be optimized, but it is harder to memoize properly.573574**Correct: primitive props enable shallow comparison**575576```tsx577const UserRow = memo(function UserRow({578 id,579 name,580 email,581}: {582 id: string583 name: string584 email: string585}) {586 // memo() compares each primitive directly587 // Re-renders only if id, name, or email actually changed588 return <Text>{name}</Text>589})590591renderItem={({ item }) => (592 <UserRow id={item.id} name={item.name} email={item.email} />593)}594```595596**Pass only what you need:**597598```tsx599// Incorrect: passing entire item when you only need name600<UserRow user={item} />601602// Correct: pass only the fields the component uses603<UserRow name={item.name} avatarUrl={item.avatar} />604```605606**For callbacks, hoist or use item ID:**607608```tsx609// Incorrect: inline function creates new reference610<UserRow name={item.name} onPress={() => handlePress(item.id)} />611612// Correct: pass ID, handle in child613<UserRow id={item.id} name={item.name} />614615const UserRow = memo(function UserRow({ id, name }: Props) {616 const handlePress = useCallback(() => {617 // use id here618 }, [id])619 return <Pressable onPress={handlePress}><Text>{name}</Text></Pressable>620})621```622623Primitive props make memoization predictable and effective.624625**Note:** If you have the React Compiler enabled, you do not need to use626627`memo()` or `useCallback()`, but the object references still apply.628629### 2.6 Use a List Virtualizer for Any List630631**Impact: HIGH (reduced memory, faster mounts)**632633Use a list virtualizer like LegendList or FlashList instead of ScrollView with634635mapped children—even for short lists. Virtualizers only render visible items,636637reducing memory usage and mount time. ScrollView renders all children upfront,638639which gets expensive quickly.640641**Incorrect: ScrollView renders all items at once**642643```tsx644function Feed({ items }: { items: Item[] }) {645 return (646 <ScrollView>647 {items.map((item) => (648 <ItemCard key={item.id} item={item} />649 ))}650 </ScrollView>651 )652}653// 50 items = 50 components mounted, even if only 10 visible654```655656**Correct: virtualizer renders only visible items**657658```tsx659import { LegendList } from '@legendapp/list'660661function Feed({ items }: { items: Item[] }) {662 return (663 <LegendList664 data={items}665 // if you aren't using React Compiler, wrap these with useCallback666 renderItem={({ item }) => <ItemCard item={item} />}667 keyExtractor={(item) => item.id}668 estimatedItemSize={80}669 />670 )671}672// Only ~10-15 visible items mounted at a time673```674675**Alternative: FlashList**676677```tsx678import { FlashList } from '@shopify/flash-list'679680function Feed({ items }: { items: Item[] }) {681 return (682 <FlashList683 data={items}684 // if you aren't using React Compiler, wrap these with useCallback685 renderItem={({ item }) => <ItemCard item={item} />}686 keyExtractor={(item) => item.id}687 />688 )689}690```691692Benefits apply to any screen with scrollable content—profiles, settings, feeds,693694search results. Default to virtualization.695696### 2.7 Use Compressed Images in Lists697698**Impact: HIGH (faster load times, less memory)**699700Always load compressed, appropriately-sized images in lists. Full-resolution701702images consume excessive memory and cause scroll jank. Request thumbnails from703704your server or use an image CDN with resize parameters.705706**Incorrect: full-resolution images**707708```tsx709function ProductItem({ product }: { product: Product }) {710 return (711 <View>712 {/* 4000x3000 image loaded for a 100x100 thumbnail */}713 <Image714 source={{ uri: product.imageUrl }}715 style={{ width: 100, height: 100 }}716 />717 <Text>{product.name}</Text>718 </View>719 )720}721```722723**Correct: request appropriately-sized image**724725```tsx726function ProductItem({ product }: { product: Product }) {727 // Request a 200x200 image (2x for retina)728 const thumbnailUrl = `${product.imageUrl}?w=200&h=200&fit=cover`729730 return (731 <View>732 <Image733 source={{ uri: thumbnailUrl }}734 style={{ width: 100, height: 100 }}735 contentFit='cover'736 />737 <Text>{product.name}</Text>738 </View>739 )740}741```742743Use an optimized image component with built-in caching and placeholder support,744745such as `expo-image` or `SolitoImage` (which uses `expo-image` under the hood).746747Request images at 2x the display size for retina screens.748749### 2.8 Use Item Types for Heterogeneous Lists750751**Impact: HIGH (efficient recycling, less layout thrashing)**752753When a list has different item layouts (messages, images, headers, etc.), use a754755`type` field on each item and provide `getItemType` to the list. This puts items756757into separate recycling pools so a message component never gets recycled into an758759image component.760761[LegendList getItemType](https://legendapp.com/open-source/list/api/props/#getitemtype-v2)762763**Incorrect: single component with conditionals**764765```tsx766type Item = { id: string; text?: string; imageUrl?: string; isHeader?: boolean }767768function ListItem({ item }: { item: Item }) {769 if (item.isHeader) {770 return <HeaderItem title={item.text} />771 }772 if (item.imageUrl) {773 return <ImageItem url={item.imageUrl} />774 }775 return <MessageItem text={item.text} />776}777778function Feed({ items }: { items: Item[] }) {779 return (780 <LegendList781 data={items}782 renderItem={({ item }) => <ListItem item={item} />}783 recycleItems784 />785 )786}787```788789**Correct: typed items with separate components**790791```tsx792type HeaderItem = { id: string; type: 'header'; title: string }793type MessageItem = { id: string; type: 'message'; text: string }794type ImageItem = { id: string; type: 'image'; url: string }795type FeedItem = HeaderItem | MessageItem | ImageItem796797function Feed({ items }: { items: FeedItem[] }) {798 return (799 <LegendList800 data={items}801 keyExtractor={(item) => item.id}802 getItemType={(item) => item.type}803 renderItem={({ item }) => {804 switch (item.type) {805 case 'header':806 return <SectionHeader title={item.title} />807 case 'message':808 return <MessageRow text={item.text} />809 case 'image':810 return <ImageRow url={item.url} />811 }812 }}813 recycleItems814 />815 )816}817```818819**Why this matters:**820821```tsx822<LegendList823 data={items}824 keyExtractor={(item) => item.id}825 getItemType={(item) => item.type}826 getEstimatedItemSize={(index, item, itemType) => {827 switch (itemType) {828 case 'header':829 return 48830 case 'message':831 return 72832 case 'image':833 return 300834 default:835 return 72836 }837 }}838 renderItem={({ item }) => {839 /* ... */840 }}841 recycleItems842/>843```844845- **Recycling efficiency**: Items with the same type share a recycling pool846847- **No layout thrashing**: A header never recycles into an image cell848849- **Type safety**: TypeScript can narrow the item type in each branch850851- **Better size estimation**: Use `getEstimatedItemSize` with `itemType` for852853 accurate estimates per type854855---856857## 3. Animation858859**Impact: HIGH**860861GPU-accelerated animations, Reanimated patterns, and avoiding862render thrashing during gestures.863864### 3.1 Animate Transform and Opacity Instead of Layout Properties865866**Impact: HIGH (GPU-accelerated animations, no layout recalculation)**867868Avoid 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.869870**Incorrect: animates height, triggers layout every frame**871872```tsx873import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'874875function CollapsiblePanel({ expanded }: { expanded: boolean }) {876 const animatedStyle = useAnimatedStyle(() => ({877 height: withTiming(expanded ? 200 : 0), // triggers layout on every frame878 overflow: 'hidden',879 }))880881 return <Animated.View style={animatedStyle}>{children}</Animated.View>882}883```884885**Correct: animates scaleY, GPU-accelerated**886887```tsx888import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'889890function CollapsiblePanel({ expanded }: { expanded: boolean }) {891 const animatedStyle = useAnimatedStyle(() => ({892 transform: [893 { scaleY: withTiming(expanded ? 1 : 0) },894 ],895 opacity: withTiming(expanded ? 1 : 0),896 }))897898 return (899 <Animated.View style={[{ height: 200, transformOrigin: 'top' }, animatedStyle]}>900 {children}901 </Animated.View>902 )903}904```905906**Correct: animates translateY for slide animations**907908```tsx909import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'910911function SlideIn({ visible }: { visible: boolean }) {912 const animatedStyle = useAnimatedStyle(() => ({913 transform: [914 { translateY: withTiming(visible ? 0 : 100) },915 ],916 opacity: withTiming(visible ? 1 : 0),917 }))918919 return <Animated.View style={animatedStyle}>{children}</Animated.View>920}921```922923GPU-accelerated properties: `transform` (translate, scale, rotate), `opacity`. Everything else triggers layout.924925### 3.2 Prefer useDerivedValue Over useAnimatedReaction926927**Impact: MEDIUM (cleaner code, automatic dependency tracking)**928929When deriving a shared value from another, use `useDerivedValue` instead of930931`useAnimatedReaction`. Derived values are declarative, automatically track932933dependencies, and return a value you can use directly. Animated reactions are934935for side effects, not derivations.936937[Reanimated useDerivedValue](https://docs.swmansion.com/react-native-reanimated/docs/core/useDerivedValue)938939**Incorrect: useAnimatedReaction for derivation**940941```tsx942import { useSharedValue, useAnimatedReaction } from 'react-native-reanimated'943944function MyComponent() {945 const progress = useSharedValue(0)946 const opacity = useSharedValue(1)947948 useAnimatedReaction(949 () => progress.value,950 (current) => {951 opacity.value = 1 - current952 }953 )954955 // ...956}957```958959**Correct: useDerivedValue**960961```tsx962import { useSharedValue, useDerivedValue } from 'react-native-reanimated'963964function MyComponent() {965 const progress = useSharedValue(0)966967 const opacity = useDerivedValue(() => 1 - progress.get())968969 // ...970}971```972973Use `useAnimatedReaction` only for side effects that don't produce a value974975(e.g., triggering haptics, logging, calling `runOnJS`).976977### 3.3 Use GestureDetector for Animated Press States978979**Impact: MEDIUM (UI thread animations, smoother press feedback)**980981For animated press states (scale, opacity on press), use `GestureDetector` with982983`Gesture.Tap()` and shared values instead of Pressable's984985`onPressIn`/`onPressOut`. Gesture callbacks run on the UI thread as worklets—no986987JS thread round-trip for press animations.988989[Gesture Handler Tap Gesture](https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture)990991**Incorrect: Pressable with JS thread callbacks**992993```tsx994import { Pressable } from 'react-native'995import Animated, {996 useSharedValue,997 useAnimatedStyle,998 withTiming,999} from 'react-native-reanimated'10001001function AnimatedButton({ onPress }: { onPress: () => void }) {1002 const scale = useSharedValue(1)10031004 const animatedStyle = useAnimatedStyle(() => ({1005 transform: [{ scale: scale.value }],1006 }))10071008 return (1009 <Pressable1010 onPress={onPress}1011 onPressIn={() => (scale.value = withTiming(0.95))}1012 onPressOut={() => (scale.value = withTiming(1))}1013 >1014 <Animated.View style={animatedStyle}>1015 <Text>Press me</Text>1016 </Animated.View>1017 </Pressable>1018 )1019}1020```10211022**Correct: GestureDetector with UI thread worklets**10231024```tsx1025import { Gesture, GestureDetector } from 'react-native-gesture-handler'1026import Animated, {1027 useSharedValue,1028 useAnimatedStyle,1029 withTiming,1030 interpolate,1031 runOnJS,1032} from 'react-native-reanimated'10331034function AnimatedButton({ onPress }: { onPress: () => void }) {1035 // Store the press STATE (0 = not pressed, 1 = pressed)1036 const pressed = useSharedValue(0)10371038 const tap = Gesture.Tap()1039 .onBegin(() => {1040 pressed.set(withTiming(1))1041 })1042 .onFinalize(() => {1043 pressed.set(withTiming(0))1044 })1045 .onEnd(() => {1046 runOnJS(onPress)()1047 })10481049 // Derive visual values from the state1050 const animatedStyle = useAnimatedStyle(() => ({1051 transform: [1052 { scale: interpolate(withTiming(pressed.get()), [0, 1], [1, 0.95]) },1053 ],1054 }))10551056 return (1057 <GestureDetector gesture={tap}>1058 <Animated.View style={animatedStyle}>1059 <Text>Press me</Text>1060 </Animated.View>1061 </GestureDetector>1062 )1063}1064```10651066Store the press **state** (0 or 1), then derive the scale via `interpolate`.10671068This keeps the shared value as ground truth. Use `runOnJS` to call JS functions10691070from worklets. Use `.set()` and `.get()` for React Compiler compatibility.10711072---10731074## 4. Scroll Performance10751076**Impact: HIGH**10771078Tracking scroll position without causing render thrashing.10791080### 4.1 Never Track Scroll Position in useState10811082**Impact: HIGH (prevents render thrashing during scroll)**10831084Never store scroll position in `useState`. Scroll events fire rapidly—state10851086updates cause render thrashing and dropped frames. Use a Reanimated shared value10871088for animations or a ref for non-reactive tracking.10891090**Incorrect: useState causes jank**10911092```tsx1093import { useState } from 'react'1094import {1095 ScrollView,1096 NativeSyntheticEvent,1097 NativeScrollEvent,1098} from 'react-native'10991100function Feed() {1101 const [scrollY, setScrollY] = useState(0)11021103 const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {1104 setScrollY(e.nativeEvent.contentOffset.y) // re-renders on every frame1105 }11061107 return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />1108}1109```11101111**Correct: Reanimated for animations**11121113```tsx1114import Animated, {1115 useSharedValue,1116 useAnimatedScrollHandler,1117} from 'react-native-reanimated'11181119function Feed() {1120 const scrollY = useSharedValue(0)11211122 const onScroll = useAnimatedScrollHandler({1123 onScroll: (e) => {1124 scrollY.value = e.contentOffset.y // runs on UI thread, no re-render1125 },1126 })11271128 return (1129 <Animated.ScrollView1130 onScroll={onScroll}1131 // higher number has better performance, but it fires less often.1132 // unset this if you need higher precision over performance.1133 scrollEventThrottle={16}1134 />1135 )1136}1137```11381139**Correct: ref for non-reactive tracking**11401141```tsx1142import { useRef } from 'react'1143import {1144 ScrollView,1145 NativeSyntheticEvent,1146 NativeScrollEvent,1147} from 'react-native'11481149function Feed() {1150 const scrollY = useRef(0)11511152 const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {1153 scrollY.current = e.nativeEvent.contentOffset.y // no re-render1154 }11551156 return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />1157}1158```11591160---11611162## 5. Navigation11631164**Impact: HIGH**11651166Using native navigators for stack and tab navigation instead of1167JS-based alternatives.11681169### 5.1 Use Native Navigators for Navigation11701171**Impact: HIGH (native performance, platform-appropriate UI)**11721173Always use native navigators instead of JS-based ones. Native navigators use11741175platform APIs (UINavigationController on iOS, Fragment on Android) for better11761177performance and native behavior.11781179**For stacks:** Use `@react-navigation/native-stack` or expo-router's default11801181stack (which uses native-stack). Avoid `@react-navigation/stack`.11821183**For tabs:** Use `react-native-bottom-tabs` (native) or expo-router's native11841185tabs. Avoid `@react-navigation/bottom-tabs` when native feel matters.11861187- [React Navigation Native Stack](https://reactnavigation.org/docs/native-stack-navigator)11881189- [React Native Bottom Tabs with React Navigation](https://oss.callstack.com/react-native-bottom-tabs/docs/guides/usage-with-react-navigation)11901191- [React Native Bottom Tabs with Expo Router](https://oss.callstack.com/react-native-bottom-tabs/docs/guides/usage-with-expo-router)11921193- [Expo Router Native Tabs](https://docs.expo.dev/router/advanced/native-tabs)11941195**Incorrect: JS stack navigator**11961197```tsx1198import { createStackNavigator } from '@react-navigation/stack'11991200const Stack = createStackNavigator()12011202function App() {1203 return (1204 <Stack.Navigator>1205 <Stack.Screen name='Home' component={HomeScreen} />1206 <Stack.Screen name='Details' component={DetailsScreen} />1207 </Stack.Navigator>1208 )1209}1210```12111212**Correct: native stack with react-navigation**12131214```tsx1215import { createNativeStackNavigator } from '@react-navigation/native-stack'12161217const Stack = createNativeStackNavigator()12181219function App() {1220 return (1221 <Stack.Navigator>1222 <Stack.Screen name='Home' component={HomeScreen} />1223 <Stack.Screen name='Details' component={DetailsScreen} />1224 </Stack.Navigator>1225 )1226}1227```12281229**Correct: expo-router uses native stack by default**12301231```tsx1232// app/_layout.tsx1233import { Stack } from 'expo-router'12341235export default function Layout() {1236 return <Stack />1237}1238```12391240**Incorrect: JS bottom tabs**12411242```tsx1243import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'12441245const Tab = createBottomTabNavigator()12461247function App() {1248 return (1249 <Tab.Navigator>1250 <Tab.Screen name='Home' component={HomeScreen} />1251 <Tab.Screen name='Settings' component={SettingsScreen} />1252 </Tab.Navigator>1253 )1254}1255```12561257**Correct: native bottom tabs with react-navigation**12581259```tsx1260import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation'12611262const Tab = createNativeBottomTabNavigator()12631264function App() {1265 return (1266 <Tab.Navigator>1267 <Tab.Screen1268 name='Home'1269 component={HomeScreen}1270 options={{1271 tabBarIcon: () => ({ sfSymbol: 'house' }),1272 }}1273 />1274 <Tab.Screen1275 name='Settings'1276 component={SettingsScreen}1277 options={{1278 tabBarIcon: () => ({ sfSymbol: 'gear' }),1279 }}1280 />1281 </Tab.Navigator>1282 )1283}1284```12851286**Correct: expo-router native tabs**12871288```tsx1289// app/(tabs)/_layout.tsx1290import { NativeTabs } from 'expo-router/unstable-native-tabs'12911292export default function TabLayout() {1293 return (1294 <NativeTabs>1295 <NativeTabs.Trigger name='index'>1296 <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>1297 <NativeTabs.Trigger.Icon sf='house.fill' md='home' />1298 </NativeTabs.Trigger>1299 <NativeTabs.Trigger name='settings'>1300 <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>1301 <NativeTabs.Trigger.Icon sf='gear' md='settings' />1302 </NativeTabs.Trigger>1303 </NativeTabs>1304 )1305}1306```13071308On iOS, native tabs automatically enable `contentInsetAdjustmentBehavior` on the13091310first `ScrollView` at the root of each tab screen, so content scrolls correctly13111312behind the translucent tab bar. If you need to disable this, use13131314`disableAutomaticContentInsets` on the trigger.13151316**Incorrect: custom header component**13171318```tsx1319<Stack.Screen1320 name='Profile'1321 component={ProfileScreen}1322 options={{1323 header: () => <CustomHeader title='Profile' />,1324 }}1325/>1326```13271328**Correct: native header options**13291330```tsx1331<Stack.Screen1332 name='Profile'1333 component={ProfileScreen}1334 options={{1335 title: 'Profile',1336 headerLargeTitleEnabled: true,1337 headerSearchBarOptions: {1338 placeholder: 'Search',1339 },1340 }}1341/>1342```13431344Native headers support iOS large titles, search bars, blur effects, and proper13451346safe area handling automatically.13471348- **Performance**: Native transitions and gestures run on the UI thread13491350- **Platform behavior**: Automatic iOS large titles, Android material design13511352- **System integration**: Scroll-to-top on tab tap, PiP avoidance, proper safe13531354 areas13551356- **Accessibility**: Platform accessibility features work automatically13571358---13591360## 6. React State13611362**Impact: MEDIUM**13631364Patterns for managing React state to avoid stale closures and1365unnecessary re-renders.13661367### 6.1 Minimize State Variables and Derive Values13681369**Impact: MEDIUM (fewer re-renders, less state drift)**13701371Use 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.13721373**Incorrect: redundant state**13741375```tsx1376function Cart({ items }: { items: Item[] }) {1377 const [total, setTotal] = useState(0)1378 const [itemCount, setItemCount] = useState(0)13791380 useEffect(() => {1381 setTotal(items.reduce((sum, item) => sum + item.price, 0))1382 setItemCount(items.length)1383 }, [items])13841385 return (1386 <View>1387 <Text>{itemCount} items</Text>1388 <Text>Total: ${total}</Text>1389 </View>1390 )1391}1392```13931394**Correct: derived values**13951396```tsx1397function Cart({ items }: { items: Item[] }) {1398 const total = items.reduce((sum, item) => sum + item.price, 0)1399 const itemCount = items.length14001401 return (1402 <View>1403 <Text>{itemCount} items</Text>1404 <Text>Total: ${total}</Text>1405 </View>1406 )1407}1408```14091410**Another example:**14111412```tsx1413// Incorrect: storing both firstName, lastName, AND fullName1414const [firstName, setFirstName] = useState('')1415const [lastName, setLastName] = useState('')1416const [fullName, setFullName] = useState('')14171418// Correct: derive fullName1419const [firstName, setFirstName] = useState('')1420const [lastName, setLastName] = useState('')1421const fullName = `${firstName} ${lastName}`1422```14231424State should be the minimal source of truth. Everything else is derived.14251426Reference: [https://react.dev/learn/choosing-the-state-structure](https://react.dev/learn/choosing-the-state-structure)14271428### 6.2 Use fallback state instead of initialState14291430**Impact: MEDIUM (reactive fallbacks without syncing)**14311432Use `undefined` as initial state and nullish coalescing (`??`) to fall back to14331434parent or server values. State represents user intent only—`undefined` means14351436"user hasn't chosen yet." This enables reactive fallbacks that update when the14371438source changes, not just on initial render.14391440**Incorrect: syncs state, loses reactivity**14411442```tsx1443type Props = { fallbackEnabled: boolean }14441445function Toggle({ fallbackEnabled }: Props) {1446 const [enabled, setEnabled] = useState(defaultEnabled)1447 // If fallbackEnabled changes, state is stale1448 // State mixes user intent with default value14491450 return <Switch value={enabled} onValueChange={setEnabled} />1451}1452```14531454**Correct: state is user intent, reactive fallback**14551456```tsx1457type Props = { fallbackEnabled: boolean }14581459function Toggle({ fallbackEnabled }: Props) {1460 const [_enabled, setEnabled] = useState<boolean | undefined>(undefined)1461 const enabled = _enabled ?? defaultEnabled1462 // undefined = user hasn't touched it, falls back to prop1463 // If defaultEnabled changes, component reflects it1464 // Once user interacts, their choice persists14651466 return <Switch value={enabled} onValueChange={setEnabled} />1467}1468```14691470**With server data:**14711472```tsx1473function ProfileForm({ data }: { data: User }) {1474 const [_theme, setTheme] = useState<string | undefined>(undefined)1475 const theme = _theme ?? data.theme1476 // Shows server value until user overrides1477 // Server refetch updates the fallback automatically14781479 return <ThemePicker value={theme} onChange={setTheme} />1480}1481```14821483### 6.3 useState Dispatch updaters for State That Depends on Current Value14841485**Impact: MEDIUM (avoids stale closures, prevents unnecessary re-renders)**14861487When the next state depends on the current state, use a dispatch updater14881489(`setState(prev => ...)`) instead of reading the state variable directly in a14901491callback. This avoids stale closures and ensures you're comparing against the14921493latest value.14941495**Incorrect: reads state directly**14961497```tsx1498const [size, setSize] = useState<Size | undefined>(undefined)14991500const onLayout = (e: LayoutChangeEvent) => {1501 const { width, height } = e.nativeEvent.layout1502 // size may be stale in this closure1503 if (size?.width !== width || size?.height !== height) {1504 setSize({ width, height })1505 }1506}1507```15081509**Correct: dispatch updater**15101511```tsx1512const [size, setSize] = useState<Size | undefined>(undefined)15131514const onLayout = (e: LayoutChangeEvent) => {1515 const { width, height } = e.nativeEvent.layout1516 setSize((prev) => {1517 if (prev?.width === width && prev?.height === height) return prev1518 return { width, height }1519 })1520}1521```15221523Returning the previous value from the updater skips the re-render.15241525For primitive states, you don't need to compare values before firing a15261527re-render.15281529**Incorrect: unnecessary comparison for primitive state**15301531```tsx1532const [size, setSize] = useState<Size | undefined>(undefined)15331534const onLayout = (e: LayoutChangeEvent) => {1535 const { width, height } = e.nativeEvent.layout1536 setSize((prev) => (prev === width ? prev : width))1537}1538```15391540**Correct: sets primitive state directly**15411542```tsx1543const [size, setSize] = useState<Size | undefined>(undefined)15441545const onLayout = (e: LayoutChangeEvent) => {1546 const { width, height } = e.nativeEvent.layout1547 setSize(width)1548}1549```15501551However, if the next state depends on the current state, you should still use a15521553dispatch updater.15541555**Incorrect: reads state directly from the callback**15561557```tsx1558const [count, setCount] = useState(0)15591560const onTap = () => {1561 setCount(count + 1)1562}1563```15641565**Correct: dispatch updater**15661567```tsx1568const [count, setCount] = useState(0)15691570const onTap = () => {1571 setCount((prev) => prev + 1)1572}1573```15741575---15761577## 7. State Architecture15781579**Impact: MEDIUM**15801581Ground truth principles for state variables and derived values.15821583### 7.1 State Must Represent Ground Truth15841585**Impact: HIGH (cleaner logic, easier debugging, single source of truth)**15861587State variables—both React `useState` and Reanimated shared values—should15881589represent the actual state of something (e.g., `pressed`, `progress`, `isOpen`),15901591not derived visual values (e.g., `scale`, `opacity`, `translateY`). Derive15921593visual values from state using computation or interpolation.15941595**Incorrect: storing the visual output**15961597```tsx1598const scale = useSharedValue(1)15991600const tap = Gesture.Tap()1601 .onBegin(() => {1602 scale.set(withTiming(0.95))1603 })1604 .onFinalize(() => {1605 scale.set(withTiming(1))1606 })16071608const animatedStyle = useAnimatedStyle(() => ({1609 transform: [{ scale: scale.get() }],1610}))1611```16121613**Correct: storing the state, deriving the visual**16141615```tsx1616const pressed = useSharedValue(0) // 0 = not pressed, 1 = pressed16171618const tap = Gesture.Tap()1619 .onBegin(() => {1620 pressed.set(withTiming(1))1621 })1622 .onFinalize(() => {1623 pressed.set(withTiming(0))1624 })16251626const animatedStyle = useAnimatedStyle(() => ({1627 transform: [{ scale: interpolate(pressed.get(), [0, 1], [1, 0.95]) }],1628}))1629```16301631**Why this matters:**16321633State variables should represent real "state", not necessarily a desired end16341635result.163616371. **Single source of truth** — The state (`pressed`) describes what's16381639 happening; visuals are derived164016412. **Easier to extend** — Adding opacity, rotation, or other effects just16421643 requires more interpolations from the same state164416453. **Debugging** — Inspecting `pressed = 1` is clearer than `scale = 0.95`164616474. **Reusable logic** — The same `pressed` value can drive multiple visual16481649 properties16501651**Same principle for React state:**16521653```tsx1654// Incorrect: storing derived values1655const [isExpanded, setIsExpanded] = useState(false)1656const [height, setHeight] = useState(0)16571658useEffect(() => {1659 setHeight(isExpanded ? 200 : 0)1660}, [isExpanded])16611662// Correct: derive from state1663const [isExpanded, setIsExpanded] = useState(false)1664const height = isExpanded ? 200 : 01665```16661667State is the minimal truth. Everything else is derived.16681669---16701671## 8. React Compiler16721673**Impact: MEDIUM**16741675Compatibility patterns for React Compiler with React Native and1676Reanimated.16771678### 8.1 Destructure Functions Early in Render (React Compiler)16791680**Impact: HIGH (stable references, fewer re-renders)**16811682This rule is only applicable if you are using the React Compiler.16831684Destructure functions from hooks at the top of render scope. Never dot into16851686objects to call functions. Destructured functions are stable references; dotting16871688creates new references and breaks memoization.16891690**Incorrect: dotting into object**16911692```tsx1693import { useRouter } from 'expo-router'16941695function SaveButton(props) {1696 const router = useRouter()16971698 // bad: react-compiler will key the cache on "props" and "router", which are objects that change each render1699 const handlePress = () => {1700 props.onSave()1701 router.push('/success') // unstable reference1702 }17031704 return <Button onPress={handlePress}>Save</Button>1705}1706```17071708**Correct: destructure early**17091710```tsx1711import { useRouter } from 'expo-router'17121713function SaveButton({ onSave }) {1714 const { push } = useRouter()17151716 // good: react-compiler will key on push and onSave1717 const handlePress = () => {1718 onSave()1719 push('/success') // stable reference1720 }17211722 return <Button onPress={handlePress}>Save</Button>1723}1724```17251726### 8.2 Use .get() and .set() for Reanimated Shared Values (not .value)17271728**Impact: LOW (required for React Compiler compatibility)**17291730With React Compiler enabled, use `.get()` and `.set()` instead of reading or17311732writing `.value` directly on Reanimated shared values. The compiler can't track17331734property access—explicit methods ensure correct behavior.17351736**Incorrect: breaks with React Compiler**17371738```tsx1739import { useSharedValue } from 'react-native-reanimated'17401741function Counter() {1742 const count = useSharedValue(0)17431744 const increment = () => {1745 count.value = count.value + 1 // opts out of react compiler1746 }17471748 return <Button onPress={increment} title={`Count: ${count.value}`} />1749}1750```17511752**Correct: React Compiler compatible**17531754```tsx1755import { useSharedValue } from 'react-native-reanimated'17561757function Counter() {1758 const count = useSharedValue(0)17591760 const increment = () => {1761 count.set(count.get() + 1)1762 }17631764 return <Button onPress={increment} title={`Count: ${count.get()}`} />1765}1766```17671768See the17691770[Reanimated docs](https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support)17711772for more.17731774---17751776## 9. User Interface17771778**Impact: MEDIUM**17791780Native UI patterns for images, menus, modals, styling, and1781platform-consistent interfaces.17821783### 9.1 Measuring View Dimensions17841785**Impact: MEDIUM (synchronous measurement, avoid unnecessary re-renders)**17861787Use both `useLayoutEffect` (synchronous) and `onLayout` (for updates). The sync17881789measurement gives you the initial size immediately; `onLayout` keeps it current17901791when the view changes. For non-primitive states, use a dispatch updater to17921793compare values and avoid unnecessary re-renders.17941795**Height only:**17961797```tsx1798import { useLayoutEffect, useRef, useState } from 'react'1799import { View, LayoutChangeEvent } from 'react-native'18001801function MeasuredBox({ children }: { children: React.ReactNode }) {1802 const ref = useRef<View>(null)1803 const [height, setHeight] = useState<number | undefined>(undefined)18041805 useLayoutEffect(() => {1806 // Sync measurement on mount (RN 0.82+)1807 const rect = ref.current?.getBoundingClientRect()1808 if (rect) setHeight(rect.height)1809 // Pre-0.82: ref.current?.measure((x, y, w, h) => setHeight(h))1810 }, [])18111812 const onLayout = (e: LayoutChangeEvent) => {1813 setHeight(e.nativeEvent.layout.height)1814 }18151816 return (1817 <View ref={ref} onLayout={onLayout}>1818 {children}1819 </View>1820 )1821}1822```18231824**Both dimensions:**18251826```tsx1827import { useLayoutEffect, useRef, useState } from 'react'1828import { View, LayoutChangeEvent } from 'react-native'18291830type Size = { width: number; height: number }18311832function MeasuredBox({ children }: { children: React.ReactNode }) {1833 const ref = useRef<View>(null)1834 const [size, setSize] = useState<Size | undefined>(undefined)18351836 useLayoutEffect(() => {1837 const rect = ref.current?.getBoundingClientRect()1838 if (rect) setSize({ width: rect.width, height: rect.height })1839 }, [])18401841 const onLayout = (e: LayoutChangeEvent) => {1842 const { width, height } = e.nativeEvent.layout1843 setSize((prev) => {1844 // for non-primitive states, compare values before firing a re-render1845 if (prev?.width === width && prev?.height === height) return prev1846 return { width, height }1847 })1848 }18491850 return (1851 <View ref={ref} onLayout={onLayout}>1852 {children}1853 </View>1854 )1855}1856```18571858Use functional setState to compare—don't read state directly in the callback.18591860### 9.2 Modern React Native Styling Patterns18611862**Impact: MEDIUM (consistent design, smoother borders, cleaner layouts)**18631864Follow these styling patterns for cleaner, more consistent React Native code.18651866**Always use `borderCurve: 'continuous'` with `borderRadius`:**18671868**Use `gap` instead of margin for spacing between elements:**18691870```tsx1871// Incorrect – margin on children1872<View>1873 <Text style={{ marginBottom: 8 }}>Title</Text>1874 <Text style={{ marginBottom: 8 }}>Subtitle</Text>1875</View>18761877// Correct – gap on parent1878<View style={{ gap: 8 }}>1879 <Text>Title</Text>1880 <Text>Subtitle</Text>1881</View>1882```18831884**Use `padding` for space within, `gap` for space between:**18851886```tsx1887<View style={{ padding: 16, gap: 12 }}>1888 <Text>First</Text>1889 <Text>Second</Text>1890</View>1891```18921893**Use `experimental_backgroundImage` for linear gradients:**18941895```tsx1896// Incorrect – third-party gradient library1897<LinearGradient colors={['#000', '#fff']} />18981899// Correct – native CSS gradient syntax1900<View1901 style={{1902 experimental_backgroundImage: 'linear-gradient(to bottom, #000, #fff)',1903 }}1904/>1905```19061907**Use CSS `boxShadow` string syntax for shadows:**19081909```tsx1910// Incorrect – legacy shadow objects or elevation1911{ shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1 }1912{ elevation: 4 }19131914// Correct – CSS box-shadow syntax1915{ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' }1916```19171918**Avoid multiple font sizes – use weight and color for emphasis:**19191920```tsx1921// Incorrect – varying font sizes for hierarchy1922<Text style={{ fontSize: 18 }}>Title</Text>1923<Text style={{ fontSize: 14 }}>Subtitle</Text>1924<Text style={{ fontSize: 12 }}>Caption</Text>19251926// Correct – consistent size, vary weight and color1927<Text style={{ fontWeight: '600' }}>Title</Text>1928<Text style={{ color: '#666' }}>Subtitle</Text>1929<Text style={{ color: '#999' }}>Caption</Text>1930```19311932Limiting font sizes creates visual consistency. Use `fontWeight` (bold/semibold)19331934and grayscale colors for hierarchy instead.19351936### 9.3 Use contentInset for Dynamic ScrollView Spacing19371938**Impact: LOW (smoother updates, no layout recalculation)**19391940When adding space to the top or bottom of a ScrollView that may change19411942(keyboard, toolbars, dynamic content), use `contentInset` instead of padding.19431944Changing `contentInset` doesn't trigger layout recalculation—it adjusts the19451946scroll area without re-rendering content.19471948**Incorrect: padding causes layout recalculation**19491950```tsx1951function Feed({ bottomOffset }: { bottomOffset: number }) {1952 return (1953 <ScrollView contentContainerStyle={{ paddingBottom: bottomOffset }}>1954 {children}1955 </ScrollView>1956 )1957}1958// Changing bottomOffset triggers full layout recalculation1959```19601961**Correct: contentInset for dynamic spacing**19621963```tsx1964function Feed({ bottomOffset }: { bottomOffset: number }) {1965 return (1966 <ScrollView1967 contentInset={{ bottom: bottomOffset }}1968 scrollIndicatorInsets={{ bottom: bottomOffset }}1969 >1970 {children}1971 </ScrollView>1972 )1973}1974// Changing bottomOffset only adjusts scroll bounds1975```19761977Use `scrollIndicatorInsets` alongside `contentInset` to keep the scroll19781979indicator aligned. For static spacing that never changes, padding is fine.19801981### 9.4 Use contentInsetAdjustmentBehavior for Safe Areas19821983**Impact: MEDIUM (native safe area handling, no layout shifts)**19841985Use `contentInsetAdjustmentBehavior="automatic"` on the root ScrollView instead of wrapping content in SafeAreaView or manual padding. This lets iOS handle safe area insets natively with proper scroll behavior.19861987**Incorrect: SafeAreaView wrapper**19881989```tsx1990import { SafeAreaView, ScrollView, View, Text } from 'react-native'19911992function MyScreen() {1993 return (1994 <SafeAreaView style={{ flex: 1 }}>1995 <ScrollView>1996 <View>1997 <Text>Content</Text>1998 </View>1999 </ScrollView>2000 </SafeAreaView>2001 )2002}2003```20042005**Incorrect: manual safe area padding**20062007```tsx2008import { ScrollView, View, Text } from 'react-native'2009import { useSafeAreaInsets } from 'react-native-safe-area-context'20102011function MyScreen() {2012 const insets = useSafeAreaInsets()20132014 return (2015 <ScrollView contentContainerStyle={{ paddingTop: insets.top }}>2016 <View>2017 <Text>Content</Text>2018 </View>2019 </ScrollView>2020 )2021}2022```20232024**Correct: native content inset adjustment**20252026```tsx2027import { ScrollView, View, Text } from 'react-native'20282029function MyScreen() {2030 return (2031 <ScrollView contentInsetAdjustmentBehavior='automatic'>2032 <View>2033 <Text>Content</Text>2034 </View>2035 </ScrollView>2036 )2037}2038```20392040The native approach handles dynamic safe areas (keyboard, toolbars) and allows content to scroll behind the status bar naturally.20412042### 9.5 Use expo-image for Optimized Images20432044**Impact: HIGH (memory efficiency, caching, blurhash placeholders, progressive loading)**20452046Use `expo-image` instead of React Native's `Image`. It provides memory-efficient caching, blurhash placeholders, progressive loading, and better performance for lists.20472048**Incorrect: React Native Image**20492050```tsx2051import { Image } from 'react-native'20522053function Avatar({ url }: { url: string }) {2054 return <Image source={{ uri: url }} style={styles.avatar} />2055}2056```20572058**Correct: expo-image**20592060```tsx2061import { Image } from 'expo-image'20622063function Avatar({ url }: { url: string }) {2064 return <Image source={{ uri: url }} style={styles.avatar} />2065}2066```20672068**With blurhash placeholder:**20692070```tsx2071<Image2072 source={{ uri: url }}2073 placeholder={{ blurhash: 'LGF5]+Yk^6#M@-5c,1J5@[or[Q6.' }}2074 contentFit="cover"2075 transition={200}2076 style={styles.image}2077/>2078```20792080**With priority and caching:**20812082```tsx2083<Image2084 source={{ uri: url }}2085 priority="high"2086 cachePolicy="memory-disk"2087 style={styles.hero}2088/>2089```20902091**Key props:**20922093- `placeholder` — Blurhash or thumbnail while loading20942095- `contentFit` — `cover`, `contain`, `fill`, `scale-down`20962097- `transition` — Fade-in duration (ms)20982099- `priority` — `low`, `normal`, `high`21002101- `cachePolicy` — `memory`, `disk`, `memory-disk`, `none`21022103- `recyclingKey` — Unique key for list recycling21042105For cross-platform (web + native), use `SolitoImage` from `solito/image` which uses `expo-image` under the hood.21062107Reference: [https://docs.expo.dev/versions/latest/sdk/image/](https://docs.expo.dev/versions/latest/sdk/image/)21082109### 9.6 Use Galeria for Image Galleries and Lightbox21102111**Impact: MEDIUM**21122113For image galleries with lightbox (tap to fullscreen), use `@nandorojo/galeria`.21142115It provides native shared element transitions with pinch-to-zoom, double-tap21162117zoom, and pan-to-close. Works with any image component including `expo-image`.21182119**Incorrect: custom modal implementation**21202121```tsx2122function ImageGallery({ urls }: { urls: string[] }) {2123 const [selected, setSelected] = useState<string | null>(null)21242125 return (2126 <>2127 {urls.map((url) => (2128 <Pressable key={url} onPress={() => setSelected(url)}>2129 <Image source={{ uri: url }} style={styles.thumbnail} />2130 </Pressable>2131 ))}2132 <Modal visible={!!selected} onRequestClose={() => setSelected(null)}>2133 <Image source={{ uri: selected! }} style={styles.fullscreen} />2134 </Modal>2135 </>2136 )2137}2138```21392140**Correct: Galeria with expo-image**21412142```tsx2143import { Galeria } from '@nandorojo/galeria'2144import { Image } from 'expo-image'21452146function ImageGallery({ urls }: { urls: string[] }) {2147 return (2148 <Galeria urls={urls}>2149 {urls.map((url, index) => (2150 <Galeria.Image index={index} key={url}>2151 <Image source={{ uri: url }} style={styles.thumbnail} />2152 </Galeria.Image>2153 ))}2154 </Galeria>2155 )2156}2157```21582159**Single image:**21602161```tsx2162import { Galeria } from '@nandorojo/galeria'2163import { Image } from 'expo-image'21642165function Avatar({ url }: { url: string }) {2166 return (2167 <Galeria urls={[url]}>2168 <Galeria.Image>2169 <Image source={{ uri: url }} style={styles.avatar} />2170 </Galeria.Image>2171 </Galeria>2172 )2173}2174```21752176**With low-res thumbnails and high-res fullscreen:**21772178```tsx2179<Galeria urls={highResUrls}>2180 {lowResUrls.map((url, index) => (2181 <Galeria.Image index={index} key={url}>2182 <Image source={{ uri: url }} style={styles.thumbnail} />2183 </Galeria.Image>2184 ))}2185</Galeria>2186```21872188**With FlashList:**21892190```tsx2191<Galeria urls={urls}>2192 <FlashList2193 data={urls}2194 renderItem={({ item, index }) => (2195 <Galeria.Image index={index}>2196 <Image source={{ uri: item }} style={styles.thumbnail} />2197 </Galeria.Image>2198 )}2199 numColumns={3}2200 estimatedItemSize={100}2201 />2202</Galeria>2203```22042205Works with `expo-image`, `SolitoImage`, `react-native` Image, or any image22062207component.22082209Reference: [https://github.com/nandorojo/galeria](https://github.com/nandorojo/galeria)22102211### 9.7 Use Native Menus for Dropdowns and Context Menus22122213**Impact: HIGH (native accessibility, platform-consistent UX)**22142215Use native platform menus instead of custom JS implementations. Native menus22162217provide built-in accessibility, consistent platform UX, and better performance.22182219Use [zeego](https://zeego.dev) for cross-platform native menus.22202221**Incorrect: custom JS menu**22222223```tsx2224import { useState } from 'react'2225import { View, Pressable, Text } from 'react-native'22262227function MyMenu() {2228 const [open, setOpen] = useState(false)22292230 return (2231 <View>2232 <Pressable onPress={() => setOpen(!open)}>2233 <Text>Open Menu</Text>2234 </Pressable>2235 {open && (2236 <View style={{ position: 'absolute', top: 40 }}>2237 <Pressable onPress={() => console.log('edit')}>2238 <Text>Edit</Text>2239 </Pressable>2240 <Pressable onPress={() => console.log('delete')}>2241 <Text>Delete</Text>2242 </Pressable>2243 </View>2244 )}2245 </View>2246 )2247}2248```22492250**Correct: native menu with zeego**22512252```tsx2253import * as DropdownMenu from 'zeego/dropdown-menu'22542255function MyMenu() {2256 return (2257 <DropdownMenu.Root>2258 <DropdownMenu.Trigger>2259 <Pressable>2260 <Text>Open Menu</Text>2261 </Pressable>2262 </DropdownMenu.Trigger>22632264 <DropdownMenu.Content>2265 <DropdownMenu.Item key='edit' onSelect={() => console.log('edit')}>2266 <DropdownMenu.ItemTitle>Edit</DropdownMenu.ItemTitle>2267 </DropdownMenu.Item>22682269 <DropdownMenu.Item2270 key='delete'2271 destructive2272 onSelect={() => console.log('delete')}2273 >2274 <DropdownMenu.ItemTitle>Delete</DropdownMenu.ItemTitle>2275 </DropdownMenu.Item>2276 </DropdownMenu.Content>2277 </DropdownMenu.Root>2278 )2279}2280```22812282**Context menu: long-press**22832284```tsx2285import * as ContextMenu from 'zeego/context-menu'22862287function MyContextMenu() {2288 return (2289 <ContextMenu.Root>2290 <ContextMenu.Trigger>2291 <View style={{ padding: 20 }}>2292 <Text>Long press me</Text>2293 </View>2294 </ContextMenu.Trigger>22952296 <ContextMenu.Content>2297 <ContextMenu.Item key='copy' onSelect={() => console.log('copy')}>2298 <ContextMenu.ItemTitle>Copy</ContextMenu.ItemTitle>2299 </ContextMenu.Item>23002301 <ContextMenu.Item key='paste' onSelect={() => console.log('paste')}>2302 <ContextMenu.ItemTitle>Paste</ContextMenu.ItemTitle>2303 </ContextMenu.Item>2304 </ContextMenu.Content>2305 </ContextMenu.Root>2306 )2307}2308```23092310**Checkbox items:**23112312```tsx2313import * as DropdownMenu from 'zeego/dropdown-menu'23142315function SettingsMenu() {2316 const [notifications, setNotifications] = useState(true)23172318 return (2319 <DropdownMenu.Root>2320 <DropdownMenu.Trigger>2321 <Pressable>2322 <Text>Settings</Text>2323 </Pressable>2324 </DropdownMenu.Trigger>23252326 <DropdownMenu.Content>2327 <DropdownMenu.CheckboxItem2328 key='notifications'2329 value={notifications}2330 onValueChange={() => setNotifications((prev) => !prev)}2331 >2332 <DropdownMenu.ItemIndicator />2333 <DropdownMenu.ItemTitle>Notifications</DropdownMenu.ItemTitle>2334 </DropdownMenu.CheckboxItem>2335 </DropdownMenu.Content>2336 </DropdownMenu.Root>2337 )2338}2339```23402341**Submenus:**23422343```tsx2344import * as DropdownMenu from 'zeego/dropdown-menu'23452346function MenuWithSubmenu() {2347 return (2348 <DropdownMenu.Root>2349 <DropdownMenu.Trigger>2350 <Pressable>2351 <Text>Options</Text>2352 </Pressable>2353 </DropdownMenu.Trigger>23542355 <DropdownMenu.Content>2356 <DropdownMenu.Item key='home' onSelect={() => console.log('home')}>2357 <DropdownMenu.ItemTitle>Home</DropdownMenu.ItemTitle>2358 </DropdownMenu.Item>23592360 <DropdownMenu.Sub>2361 <DropdownMenu.SubTrigger key='more'>2362 <DropdownMenu.ItemTitle>More Options</DropdownMenu.ItemTitle>2363 </DropdownMenu.SubTrigger>23642365 <DropdownMenu.SubContent>2366 <DropdownMenu.Item key='settings'>2367 <DropdownMenu.ItemTitle>Settings</DropdownMenu.ItemTitle>2368 </DropdownMenu.Item>23692370 <DropdownMenu.Item key='help'>2371 <DropdownMenu.ItemTitle>Help</DropdownMenu.ItemTitle>2372 </DropdownMenu.Item>2373 </DropdownMenu.SubContent>2374 </DropdownMenu.Sub>2375 </DropdownMenu.Content>2376 </DropdownMenu.Root>2377 )2378}2379```23802381Reference: [https://zeego.dev/components/dropdown-menu](https://zeego.dev/components/dropdown-menu)23822383### 9.8 Use Native Modals Over JS-Based Bottom Sheets23842385**Impact: HIGH (native performance, gestures, accessibility)**23862387Use native `<Modal>` with `presentationStyle="formSheet"` or React Navigation23882389v7's native form sheet instead of JS-based bottom sheet libraries. Native modals23902391have built-in gestures, accessibility, and better performance. Rely on native UI23922393for low-level primitives.23942395**Incorrect: JS-based bottom sheet**23962397```tsx2398import BottomSheet from 'custom-js-bottom-sheet'23992400function MyScreen() {2401 const sheetRef = useRef<BottomSheet>(null)24022403 return (2404 <View style={{ flex: 1 }}>2405 <Button onPress={() => sheetRef.current?.expand()} title='Open' />2406 <BottomSheet ref={sheetRef} snapPoints={['50%', '90%']}>2407 <View>2408 <Text>Sheet content</Text>2409 </View>2410 </BottomSheet>2411 </View>2412 )2413}2414```24152416**Correct: native Modal with formSheet**24172418```tsx2419import { Modal, View, Text, Button } from 'react-native'24202421function MyScreen() {2422 const [visible, setVisible] = useState(false)24232424 return (2425 <View style={{ flex: 1 }}>2426 <Button onPress={() => setVisible(true)} title='Open' />2427 <Modal2428 visible={visible}2429 presentationStyle='formSheet'2430 animationType='slide'2431 onRequestClose={() => setVisible(false)}2432 >2433 <View>2434 <Text>Sheet content</Text>2435 </View>2436 </Modal>2437 </View>2438 )2439}2440```24412442**Correct: React Navigation v7 native form sheet**24432444```tsx2445// In your navigator2446<Stack.Screen2447 name='Details'2448 component={DetailsScreen}2449 options={{2450 presentation: 'formSheet',2451 sheetAllowedDetents: 'fitToContents',2452 }}2453/>2454```24552456Native modals provide swipe-to-dismiss, proper keyboard avoidance, and24572458accessibility out of the box.24592460### 9.9 Use Pressable Instead of Touchable Components24612462**Impact: LOW (modern API, more flexible)**24632464Never use `TouchableOpacity` or `TouchableHighlight`. Use `Pressable` from24652466`react-native` or `react-native-gesture-handler` instead.24672468**Incorrect: legacy Touchable components**24692470```tsx2471import { TouchableOpacity } from 'react-native'24722473function MyButton({ onPress }: { onPress: () => void }) {2474 return (2475 <TouchableOpacity onPress={onPress} activeOpacity={0.7}>2476 <Text>Press me</Text>2477 </TouchableOpacity>2478 )2479}2480```24812482**Correct: Pressable**24832484```tsx2485import { Pressable } from 'react-native'24862487function MyButton({ onPress }: { onPress: () => void }) {2488 return (2489 <Pressable onPress={onPress}>2490 <Text>Press me</Text>2491 </Pressable>2492 )2493}2494```24952496**Correct: Pressable from gesture handler for lists**24972498```tsx2499import { Pressable } from 'react-native-gesture-handler'25002501function ListItem({ onPress }: { onPress: () => void }) {2502 return (2503 <Pressable onPress={onPress}>2504 <Text>Item</Text>2505 </Pressable>2506 )2507}2508```25092510Use `react-native-gesture-handler` Pressable inside scrollable lists for better25112512gesture coordination, as long as you are using the ScrollView from25132514`react-native-gesture-handler` as well.25152516**For animated press states (scale, opacity changes):** Use `GestureDetector`25172518with Reanimated shared values instead of Pressable's style callback. See the25192520`animation-gesture-detector-press` rule.25212522---25232524## 10. Design System25252526**Impact: MEDIUM**25272528Architecture patterns for building maintainable component2529libraries.25302531### 10.1 Use Compound Components Over Polymorphic Children25322533**Impact: MEDIUM (flexible composition, clearer API)**25342535Don't create components that can accept a string if they aren't a text node. If25362537a component can receive a string child, it must be a dedicated `*Text`25382539component. For components like buttons, which can have both a View (or25402541Pressable) together with text, use compound components, such a `Button`,25422543`ButtonText`, and `ButtonIcon`.25442545**Incorrect: polymorphic children**25462547```tsx2548import { Pressable, Text } from 'react-native'25492550type ButtonProps = {2551 children: string | React.ReactNode2552 icon?: React.ReactNode2553}25542555function Button({ children, icon }: ButtonProps) {2556 return (2557 <Pressable>2558 {icon}2559 {typeof children === 'string' ? <Text>{children}</Text> : children}2560 </Pressable>2561 )2562}25632564// Usage is ambiguous2565<Button icon={<Icon />}>Save</Button>2566<Button><CustomText>Save</CustomText></Button>2567```25682569**Correct: compound components**25702571```tsx2572import { Pressable, Text } from 'react-native'25732574function Button({ children }: { children: React.ReactNode }) {2575 return <Pressable>{children}</Pressable>2576}25772578function ButtonText({ children }: { children: React.ReactNode }) {2579 return <Text>{children}</Text>2580}25812582function ButtonIcon({ children }: { children: React.ReactNode }) {2583 return <>{children}</>2584}25852586// Usage is explicit and composable2587<Button>2588 <ButtonIcon><SaveIcon /></ButtonIcon>2589 <ButtonText>Save</ButtonText>2590</Button>25912592<Button>2593 <ButtonText>Cancel</ButtonText>2594</Button>2595```25962597---25982599## 11. Monorepo26002601**Impact: LOW**26022603Dependency management and native module configuration in2604monorepos.26052606### 11.1 Install Native Dependencies in App Directory26072608**Impact: CRITICAL (required for autolinking to work)**26092610In a monorepo, packages with native code must be installed in the native app's26112612directory directly. Autolinking only scans the app's `node_modules`—it won't26132614find native dependencies installed in other packages.26152616**Incorrect: native dep in shared package only**26172618```typescript2619packages/2620 ui/2621 package.json # has react-native-reanimated2622 app/2623 package.json # missing react-native-reanimated2624```26252626Autolinking fails—native code not linked.26272628**Correct: native dep in app directory**26292630```json2631// packages/app/package.json2632{2633 "dependencies": {2634 "react-native-reanimated": "3.16.1"2635 }2636}2637```26382639Even if the shared package uses the native dependency, the app must also list it26402641for autolinking to detect and link the native code.26422643### 11.2 Use Single Dependency Versions Across Monorepo26442645**Impact: MEDIUM (avoids duplicate bundles, version conflicts)**26462647Use a single version of each dependency across all packages in your monorepo.26482649Prefer exact versions over ranges. Multiple versions cause duplicate code in26502651bundles, runtime conflicts, and inconsistent behavior across packages.26522653Use a tool like syncpack to enforce this. As a last resort, use yarn resolutions26542655or npm overrides.26562657**Incorrect: version ranges, multiple versions**26582659```json2660// packages/app/package.json2661{2662 "dependencies": {2663 "react-native-reanimated": "^3.0.0"2664 }2665}26662667// packages/ui/package.json2668{2669 "dependencies": {2670 "react-native-reanimated": "^3.5.0"2671 }2672}2673```26742675**Correct: exact versions, single source of truth**26762677```json2678// package.json (root)2679{2680 "pnpm": {2681 "overrides": {2682 "react-native-reanimated": "3.16.1"2683 }2684 }2685}26862687// packages/app/package.json2688{2689 "dependencies": {2690 "react-native-reanimated": "3.16.1"2691 }2692}26932694// packages/ui/package.json2695{2696 "dependencies": {2697 "react-native-reanimated": "3.16.1"2698 }2699}2700```27012702Use your package manager's override/resolution feature to enforce versions at27032704the root. When adding dependencies, specify exact versions without `^` or `~`.27052706---27072708## 12. Third-Party Dependencies27092710**Impact: LOW**27112712Wrapping and re-exporting third-party dependencies for2713maintainability.27142715### 12.1 Import from Design System Folder27162717**Impact: LOW (enables global changes and easy refactoring)**27182719Re-export dependencies from a design system folder. App code imports from there,27202721not directly from packages. This enables global changes and easy refactoring.27222723**Incorrect: imports directly from package**27242725```tsx2726import { View, Text } from 'react-native'2727import { Button } from '@ui/button'27282729function Profile() {2730 return (2731 <View>2732 <Text>Hello</Text>2733 <Button>Save</Button>2734 </View>2735 )2736}2737```27382739**Correct: imports from design system**27402741```tsx2742import { View } from '@/components/view'2743import { Text } from '@/components/text'2744import { Button } from '@/components/button'27452746function Profile() {2747 return (2748 <View>2749 <Text>Hello</Text>2750 <Button>Save</Button>2751 </View>2752 )2753}2754```27552756Start by simply re-exporting. Customize later without changing app code.27572758---27592760## 13. JavaScript27612762**Impact: LOW**27632764Micro-optimizations like hoisting expensive object creation.27652766### 13.1 Hoist Intl Formatter Creation27672768**Impact: LOW-MEDIUM (avoids expensive object recreation)**27692770Don't create `Intl.DateTimeFormat`, `Intl.NumberFormat`, or27712772`Intl.RelativeTimeFormat` inside render or loops. These are expensive to27732774instantiate. Hoist to module scope when the locale/options are static.27752776**Incorrect: new formatter every render**27772778```tsx2779function Price({ amount }: { amount: number }) {2780 const formatter = new Intl.NumberFormat('en-US', {2781 style: 'currency',2782 currency: 'USD',2783 })2784 return <Text>{formatter.format(amount)}</Text>2785}2786```27872788**Correct: hoisted to module scope**27892790```tsx2791const currencyFormatter = new Intl.NumberFormat('en-US', {2792 style: 'currency',2793 currency: 'USD',2794})27952796function Price({ amount }: { amount: number }) {2797 return <Text>{currencyFormatter.format(amount)}</Text>2798}2799```28002801**For dynamic locales, memoize:**28022803```tsx2804const dateFormatter = useMemo(2805 () => new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }),2806 [locale]2807)2808```28092810**Common formatters to hoist:**28112812```tsx2813// Module-level formatters2814const dateFormatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' })2815const timeFormatter = new Intl.DateTimeFormat('en-US', { timeStyle: 'short' })2816const percentFormatter = new Intl.NumberFormat('en-US', { style: 'percent' })2817const relativeFormatter = new Intl.RelativeTimeFormat('en-US', {2818 numeric: 'auto',2819})2820```28212822Creating `Intl` objects is significantly more expensive than `RegExp` or plain28232824objects—each instantiation parses locale data and builds internal lookup tables.28252826---28272828## 14. Fonts28292830**Impact: LOW**28312832Native font loading for improved performance.28332834### 14.1 Load fonts natively at build time28352836**Impact: LOW (fonts available at launch, no async loading)**28372838Use the `expo-font` config plugin to embed fonts at build time instead of28392840`useFonts` or `Font.loadAsync`. Embedded fonts are more efficient.28412842[Expo Font Documentation](https://docs.expo.dev/versions/latest/sdk/font/)28432844**Incorrect: async font loading**28452846```tsx2847import { useFonts } from 'expo-font'2848import { Text, View } from 'react-native'28492850function App() {2851 const [fontsLoaded] = useFonts({2852 'Geist-Bold': require('./assets/fonts/Geist-Bold.otf'),2853 })28542855 if (!fontsLoaded) {2856 return null2857 }28582859 return (2860 <View>2861 <Text style={{ fontFamily: 'Geist-Bold' }}>Hello</Text>2862 </View>2863 )2864}2865```28662867**Correct: config plugin, fonts embedded at build**28682869```tsx2870import { Text, View } from 'react-native'28712872function App() {2873 // No loading state needed—font is already available2874 return (2875 <View>2876 <Text style={{ fontFamily: 'Geist-Bold' }}>Hello</Text>2877 </View>2878 )2879}2880```28812882After adding fonts to the config plugin, run `npx expo prebuild` and rebuild the28832884native app.28852886---28872888## References288928901. [https://react.dev](https://react.dev)28912. [https://reactnative.dev](https://reactnative.dev)28923. [https://docs.swmansion.com/react-native-reanimated](https://docs.swmansion.com/react-native-reanimated)28934. [https://docs.swmansion.com/react-native-gesture-handler](https://docs.swmansion.com/react-native-gesture-handler)28945. [https://docs.expo.dev](https://docs.expo.dev)28956. [https://legendapp.com/open-source/legend-list](https://legendapp.com/open-source/legend-list)28967. [https://github.com/nandorojo/galeria](https://github.com/nandorojo/galeria)28978. [https://zeego.dev](https://zeego.dev)2898
One repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-build.mdc · 51 | Cursor rules | no sections | 16/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-code-simplify.mdc · 51 | Cursor rules | testing-strategy | 30/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-plan.mdc · 51 | Cursor rules | no sections | 16/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-review.mdc · 51 | Cursor rules | no sections | 16/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-ship.mdc · 51 | Cursor rules | testing-strategygitdeploymentdo-not | 61/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-spec.mdc · 51 | Cursor rules | no sections | 16/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-cursor/.cursor/rules/cmd-test.mdc · 51 | Cursor rules | no sections | 16/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-forgecat/AGENTS.md · 51 | AGENTS.md | lint-formatstylearchdo-not | 73/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/addyosmani/agent-skills/for-forgecat/CLAUDE.md · 51 | CLAUDE.md | teststylearchagent-behaviour | 70/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-code/anthropics_claude-code_ralph-wiggum/for-cursor/.cursor/rules/cmd-cancel-ralph.mdc · 51 | Cursor rules | no sections | 16/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-code/anthropics_claude-code_ralph-wiggum/for-cursor/.cursor/rules/cmd-help.mdc · 51 | Cursor rules | no sections | 54/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-code/anthropics_claude-code_ralph-wiggum/for-cursor/.cursor/rules/cmd-ralph-loop.mdc · 51 | Cursor rules | no sections | 22/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_agent-sdk-dev/for-cursor/.cursor/rules/cmd-new-sdk-app.mdc · 51 | Cursor rules | setupstylearchdocs | 76/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_claude-md-management/for-cursor/.cursor/rules/cmd-revise-claude-md.mdc · 51 | Cursor rules | agent-behaviour | 50/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_code-review/for-cursor/.cursor/rules/cmd-code-review.mdc · 51 | Cursor rules | testing-strategygit | 35/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_commit-commands/for-cursor/.cursor/rules/cmd-clean_gone.mdc · 51 | Cursor rules | no sections | 60/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_commit-commands/for-cursor/.cursor/rules/cmd-commit-push-pr.mdc · 51 | Cursor rules | stylegit | 44/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_commit-commands/for-cursor/.cursor/rules/cmd-commit.mdc · 51 | Cursor rules | style | 44/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_example-plugin/for-cursor/.cursor/rules/cmd-example-command.mdc · 51 | Cursor rules | lint-formatstyleagent-behaviour | 58/100 | today | |
| nota-america/forgecat-agent-profilesprofiles/anthropics/claude-plugins-official/anthropics_claude-plugins-official_feature-dev/for-cursor/.cursor/rules/cmd-feature-dev.mdc · 51 | Cursor rules | stylearchgit | 56/100 | today |
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| vllm-project/vllmAGENTS.md · 89k | AGENTS.md | setuptestlint-formatstyle+5 | 100/100 | 14 days ago | |
| code-yeongyu/oh-my-openagentpackages/web/AGENTS.md · 68k | AGENTS.md | setupbuildtestlint-format+6 | 100/100 | 13 days ago | |
| deepseek-ai/deepseek-harnessnative/landlock-run/AGENTS.md · 104k | AGENTS.md | setupteststylearch+3 | 100/100 | today | |
| TryGhost/Ghoste2e/AGENTS.md · 55k | AGENTS.md | setupteststylearch+2 | 100/100 | today | |
| aaif-goose/gooseAGENTS.md · 53k | AGENTS.md | setupbuildtestlint-format+7 | 100/100 | 8 days ago | |
| duckduckgo/content-scope-scriptsspecial-pages/AGENTS.md · 70 | AGENTS.md | buildteststylearch+3 | 100/100 | 14 days ago | |
| n8n-io/n8npackages/@n8n/agents/AGENTS.md · 201k | AGENTS.md | buildteststylearch+3 | 100/100 | 14 days ago | |
| mui/material-uiAGENTS.md · 99k | AGENTS.md | setupbuildtestlint-format+9 | 100/100 | 14 days ago |
A badge carrying the measured quality of the strongest agent config file in this repository, out of 100. It reads from this index every time somebody loads your page, so it changes when the measurement changes and there is nothing to keep up to date. Free, no account, and the value is not something you or we can set by hand.
[](https://rulestack.kynth.studio/configs/nota-america-forgecat-agent-profiles-profiles-vercel-labs-agent-skills-vercel-labs-agent-skills-react-native-skills-for-forgecat-agents)Would rather not hotlink us? Every badge is also served in shields.io’s endpoint schema, so shields renders the image and your readers never talk to our domain:
Published by Toolproof, the masthead over this index and eight others. The method behind the number is at toolproof.kynth.studio/methodology, and the whole thing is readable as JSON with no key at /api.