@0xsown/vibe-code-fe 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/bin/index.js +181 -0
  2. package/package.json +32 -0
  3. package/skills/claude-md-improver/SKILL.md +179 -0
  4. package/skills/claude-md-improver/references/quality-criteria.md +109 -0
  5. package/skills/claude-md-improver/references/templates.md +253 -0
  6. package/skills/claude-md-improver/references/update-guidelines.md +150 -0
  7. package/skills/find-skills/SKILL.md +133 -0
  8. package/skills/frontend-design/LICENSE.txt +177 -0
  9. package/skills/frontend-design/SKILL.md +42 -0
  10. package/skills/next-best-practices/SKILL.md +153 -0
  11. package/skills/next-best-practices/async-patterns.md +87 -0
  12. package/skills/next-best-practices/bundling.md +180 -0
  13. package/skills/next-best-practices/data-patterns.md +297 -0
  14. package/skills/next-best-practices/debug-tricks.md +105 -0
  15. package/skills/next-best-practices/directives.md +73 -0
  16. package/skills/next-best-practices/error-handling.md +227 -0
  17. package/skills/next-best-practices/file-conventions.md +140 -0
  18. package/skills/next-best-practices/font.md +245 -0
  19. package/skills/next-best-practices/functions.md +108 -0
  20. package/skills/next-best-practices/hydration-error.md +91 -0
  21. package/skills/next-best-practices/image.md +173 -0
  22. package/skills/next-best-practices/metadata.md +301 -0
  23. package/skills/next-best-practices/parallel-routes.md +287 -0
  24. package/skills/next-best-practices/route-handlers.md +146 -0
  25. package/skills/next-best-practices/rsc-boundaries.md +159 -0
  26. package/skills/next-best-practices/runtime-selection.md +39 -0
  27. package/skills/next-best-practices/scripts.md +141 -0
  28. package/skills/next-best-practices/self-hosting.md +371 -0
  29. package/skills/next-best-practices/suspense-boundaries.md +67 -0
  30. package/skills/next-cache-components/SKILL.md +411 -0
  31. package/skills/shadcn-ui/README.md +248 -0
  32. package/skills/shadcn-ui/SKILL.md +326 -0
  33. package/skills/shadcn-ui/examples/auth-layout.tsx +177 -0
  34. package/skills/shadcn-ui/examples/data-table.tsx +313 -0
  35. package/skills/shadcn-ui/examples/form-pattern.tsx +177 -0
  36. package/skills/shadcn-ui/resources/component-catalog.md +481 -0
  37. package/skills/shadcn-ui/resources/customization-guide.md +516 -0
  38. package/skills/shadcn-ui/resources/migration-guide.md +463 -0
  39. package/skills/shadcn-ui/resources/setup-guide.md +412 -0
  40. package/skills/shadcn-ui/scripts/verify-setup.sh +134 -0
  41. package/skills/supabase-postgres-best-practices/AGENTS.md +68 -0
  42. package/skills/supabase-postgres-best-practices/CLAUDE.md +68 -0
  43. package/skills/supabase-postgres-best-practices/README.md +116 -0
  44. package/skills/supabase-postgres-best-practices/SKILL.md +64 -0
  45. package/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
  46. package/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
  47. package/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
  48. package/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
  49. package/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
  50. package/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
  51. package/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
  52. package/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
  53. package/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
  54. package/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
  55. package/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
  56. package/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
  57. package/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
  58. package/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
  59. package/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
  60. package/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
  61. package/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
  62. package/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
  63. package/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
  64. package/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
  65. package/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
  66. package/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
  67. package/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
  68. package/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
  69. package/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
  70. package/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
  71. package/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
  72. package/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
  73. package/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
  74. package/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
  75. package/skills/supabase-postgres-best-practices/references/security-rls-performance.md +57 -0
  76. package/skills/tailwind-design-system/SKILL.md +874 -0
  77. package/skills/vercel-composition-patterns/AGENTS.md +946 -0
  78. package/skills/vercel-composition-patterns/README.md +60 -0
  79. package/skills/vercel-composition-patterns/SKILL.md +89 -0
  80. package/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md +100 -0
  81. package/skills/vercel-composition-patterns/rules/architecture-compound-components.md +112 -0
  82. package/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md +87 -0
  83. package/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md +100 -0
  84. package/skills/vercel-composition-patterns/rules/react19-no-forwardref.md +42 -0
  85. package/skills/vercel-composition-patterns/rules/state-context-interface.md +191 -0
  86. package/skills/vercel-composition-patterns/rules/state-decouple-implementation.md +113 -0
  87. package/skills/vercel-composition-patterns/rules/state-lift-state.md +125 -0
  88. package/skills/vercel-react-best-practices/AGENTS.md +2934 -0
  89. package/skills/vercel-react-best-practices/README.md +123 -0
  90. package/skills/vercel-react-best-practices/SKILL.md +136 -0
  91. package/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  92. package/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
  93. package/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
  94. package/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  95. package/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
  96. package/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
  97. package/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  98. package/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  99. package/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
  100. package/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  101. package/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  102. package/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  103. package/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  104. package/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  105. package/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  106. package/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  107. package/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  108. package/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
  109. package/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  110. package/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  111. package/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  112. package/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  113. package/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  114. package/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  115. package/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  116. package/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  117. package/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  118. package/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  119. package/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  120. package/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  121. package/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  122. package/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  123. package/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  124. package/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  125. package/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  126. package/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  127. package/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  128. package/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  129. package/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  130. package/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  131. package/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  132. package/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  133. package/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  134. package/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  135. package/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  136. package/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  137. package/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  138. package/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  139. package/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  140. package/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  141. package/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  142. package/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
  143. package/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  144. package/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  145. package/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
  146. package/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  147. package/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  148. package/skills/vercel-react-native-skills/AGENTS.md +2897 -0
  149. package/skills/vercel-react-native-skills/README.md +165 -0
  150. package/skills/vercel-react-native-skills/SKILL.md +121 -0
  151. package/skills/vercel-react-native-skills/rules/animation-derived-value.md +53 -0
  152. package/skills/vercel-react-native-skills/rules/animation-gesture-detector-press.md +95 -0
  153. package/skills/vercel-react-native-skills/rules/animation-gpu-properties.md +65 -0
  154. package/skills/vercel-react-native-skills/rules/design-system-compound-components.md +66 -0
  155. package/skills/vercel-react-native-skills/rules/fonts-config-plugin.md +71 -0
  156. package/skills/vercel-react-native-skills/rules/imports-design-system-folder.md +68 -0
  157. package/skills/vercel-react-native-skills/rules/js-hoist-intl.md +61 -0
  158. package/skills/vercel-react-native-skills/rules/list-performance-callbacks.md +44 -0
  159. package/skills/vercel-react-native-skills/rules/list-performance-function-references.md +132 -0
  160. package/skills/vercel-react-native-skills/rules/list-performance-images.md +53 -0
  161. package/skills/vercel-react-native-skills/rules/list-performance-inline-objects.md +97 -0
  162. package/skills/vercel-react-native-skills/rules/list-performance-item-expensive.md +94 -0
  163. package/skills/vercel-react-native-skills/rules/list-performance-item-memo.md +82 -0
  164. package/skills/vercel-react-native-skills/rules/list-performance-item-types.md +104 -0
  165. package/skills/vercel-react-native-skills/rules/list-performance-virtualize.md +67 -0
  166. package/skills/vercel-react-native-skills/rules/monorepo-native-deps-in-app.md +46 -0
  167. package/skills/vercel-react-native-skills/rules/monorepo-single-dependency-versions.md +63 -0
  168. package/skills/vercel-react-native-skills/rules/navigation-native-navigators.md +188 -0
  169. package/skills/vercel-react-native-skills/rules/react-compiler-destructure-functions.md +50 -0
  170. package/skills/vercel-react-native-skills/rules/react-compiler-reanimated-shared-values.md +48 -0
  171. package/skills/vercel-react-native-skills/rules/react-state-dispatcher.md +91 -0
  172. package/skills/vercel-react-native-skills/rules/react-state-fallback.md +56 -0
  173. package/skills/vercel-react-native-skills/rules/react-state-minimize.md +65 -0
  174. package/skills/vercel-react-native-skills/rules/rendering-no-falsy-and.md +74 -0
  175. package/skills/vercel-react-native-skills/rules/rendering-text-in-text-component.md +36 -0
  176. package/skills/vercel-react-native-skills/rules/scroll-position-no-state.md +82 -0
  177. package/skills/vercel-react-native-skills/rules/state-ground-truth.md +80 -0
  178. package/skills/vercel-react-native-skills/rules/ui-expo-image.md +66 -0
  179. package/skills/vercel-react-native-skills/rules/ui-image-gallery.md +104 -0
  180. package/skills/vercel-react-native-skills/rules/ui-measure-views.md +78 -0
  181. package/skills/vercel-react-native-skills/rules/ui-menus.md +174 -0
  182. package/skills/vercel-react-native-skills/rules/ui-native-modals.md +77 -0
  183. package/skills/vercel-react-native-skills/rules/ui-pressable.md +61 -0
  184. package/skills/vercel-react-native-skills/rules/ui-safe-area-scroll.md +65 -0
  185. package/skills/vercel-react-native-skills/rules/ui-scrollview-content-inset.md +45 -0
  186. package/skills/vercel-react-native-skills/rules/ui-styling.md +87 -0
  187. package/skills/web-design-guidelines/SKILL.md +39 -0
  188. package/templates/AGENTS.md +31 -0
  189. package/templates/CLAUDE.md +31 -0
@@ -0,0 +1,2897 @@
1
+ # React Native Skills
2
+
3
+ **Version 1.0.0**
4
+ Engineering
5
+ January 2026
6
+
7
+ > **Note:**
8
+ > This document is mainly for agents and LLMs to follow when maintaining,
9
+ > generating, or refactoring React Native codebases. Humans
10
+ > may also find it useful, but guidance here is optimized for automation
11
+ > and consistency by AI-assisted workflows.
12
+
13
+ ---
14
+
15
+ ## Abstract
16
+
17
+ 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.
18
+
19
+ ---
20
+
21
+ ## Table of Contents
22
+
23
+ 1. [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)
26
+ 2. [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)
35
+ 3. [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)
39
+ 4. [Scroll Performance](#4-scroll-performance) — **HIGH**
40
+ - 4.1 [Never Track Scroll Position in useState](#41-never-track-scroll-position-in-usestate)
41
+ 5. [Navigation](#5-navigation) — **HIGH**
42
+ - 5.1 [Use Native Navigators for Navigation](#51-use-native-navigators-for-navigation)
43
+ 6. [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)
47
+ 7. [State Architecture](#7-state-architecture) — **MEDIUM**
48
+ - 7.1 [State Must Represent Ground Truth](#71-state-must-represent-ground-truth)
49
+ 8. [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)
52
+ 9. [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)
62
+ 10. [Design System](#10-design-system) — **MEDIUM**
63
+ - 10.1 [Use Compound Components Over Polymorphic Children](#101-use-compound-components-over-polymorphic-children)
64
+ 11. [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)
67
+ 12. [Third-Party Dependencies](#12-third-party-dependencies) — **LOW**
68
+ - 12.1 [Import from Design System Folder](#121-import-from-design-system-folder)
69
+ 13. [JavaScript](#13-javascript) — **LOW**
70
+ - 13.1 [Hoist Intl Formatter Creation](#131-hoist-intl-formatter-creation)
71
+ 14. [Fonts](#14-fonts) — **LOW**
72
+ - 14.1 [Load fonts natively at build time](#141-load-fonts-natively-at-build-time)
73
+
74
+ ---
75
+
76
+ ## 1. Core Rendering
77
+
78
+ **Impact: CRITICAL**
79
+
80
+ Fundamental React Native rendering rules. Violations cause
81
+ runtime crashes or broken UI.
82
+
83
+ ### 1.1 Never Use && with Potentially Falsy Values
84
+
85
+ **Impact: CRITICAL (prevents production crash)**
86
+
87
+ Never use `{value && <Component />}` when `value` could be an empty string or
88
+
89
+ `0`. These are falsy but JSX-renderable—React Native will try to render them as
90
+
91
+ text outside a `<Text>` component, causing a hard crash in production.
92
+
93
+ **Incorrect: crashes if count is 0 or name is ""**
94
+
95
+ ```tsx
96
+ function 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 → crash
105
+ ```
106
+
107
+ **Correct: ternary with null**
108
+
109
+ ```tsx
110
+ function 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
+ ```
119
+
120
+ **Correct: explicit boolean coercion**
121
+
122
+ ```tsx
123
+ function 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
+ ```
132
+
133
+ **Best: early return**
134
+
135
+ ```tsx
136
+ function Profile({ name, count }: { name: string; count: number }) {
137
+ if (!name) return null
138
+
139
+ return (
140
+ <View>
141
+ <Text>{name}</Text>
142
+ {count > 0 ? <Text>{count} items</Text> : null}
143
+ </View>
144
+ )
145
+ }
146
+ ```
147
+
148
+ Early returns are clearest. When using conditionals inline, prefer ternary or
149
+
150
+ explicit boolean checks.
151
+
152
+ **Lint rule:** Enable `react/jsx-no-leaked-render` from
153
+
154
+ [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-leaked-render.md)
155
+
156
+ to catch this automatically.
157
+
158
+ ### 1.2 Wrap Strings in Text Components
159
+
160
+ **Impact: CRITICAL (prevents runtime crash)**
161
+
162
+ Strings must be rendered inside `<Text>`. React Native crashes if a string is a
163
+
164
+ direct child of `<View>`.
165
+
166
+ **Incorrect: crashes**
167
+
168
+ ```tsx
169
+ import { View } from 'react-native'
170
+
171
+ function Greeting({ name }: { name: string }) {
172
+ return <View>Hello, {name}!</View>
173
+ }
174
+ // Error: Text strings must be rendered within a <Text> component.
175
+ ```
176
+
177
+ **Correct:**
178
+
179
+ ```tsx
180
+ import { View, Text } from 'react-native'
181
+
182
+ function Greeting({ name }: { name: string }) {
183
+ return (
184
+ <View>
185
+ <Text>Hello, {name}!</Text>
186
+ </View>
187
+ )
188
+ }
189
+ ```
190
+
191
+ ---
192
+
193
+ ## 2. List Performance
194
+
195
+ **Impact: HIGH**
196
+
197
+ Optimizing virtualized lists (FlatList, LegendList, FlashList)
198
+ for smooth scrolling and fast updates.
199
+
200
+ ### 2.1 Avoid Inline Objects in renderItem
201
+
202
+ **Impact: HIGH (prevents unnecessary re-renders of memoized list items)**
203
+
204
+ Don't create new objects inside `renderItem` to pass as props. Inline objects
205
+
206
+ create new references on every render, breaking memoization. Pass primitive
207
+
208
+ values directly from `item` instead.
209
+
210
+ **Incorrect: inline object breaks memoization**
211
+
212
+ ```tsx
213
+ function UserList({ users }: { users: User[] }) {
214
+ return (
215
+ <LegendList
216
+ data={users}
217
+ renderItem={({ item }) => (
218
+ <UserRow
219
+ // Bad: new object on every render
220
+ user={{ id: item.id, name: item.name, avatar: item.avatar }}
221
+ />
222
+ )}
223
+ />
224
+ )
225
+ }
226
+ ```
227
+
228
+ **Incorrect: inline style object**
229
+
230
+ ```tsx
231
+ renderItem={({ item }) => (
232
+ <UserRow
233
+ name={item.name}
234
+ // Bad: new style object on every render
235
+ style={{ backgroundColor: item.isActive ? 'green' : 'gray' }}
236
+ />
237
+ )}
238
+ ```
239
+
240
+ **Correct: pass item directly or primitives**
241
+
242
+ ```tsx
243
+ function UserList({ users }: { users: User[] }) {
244
+ return (
245
+ <LegendList
246
+ data={users}
247
+ renderItem={({ item }) => (
248
+ // Good: pass the item directly
249
+ <UserRow user={item} />
250
+ )}
251
+ />
252
+ )
253
+ }
254
+ ```
255
+
256
+ **Correct: pass primitives, derive inside child**
257
+
258
+ ```tsx
259
+ renderItem={({ item }) => (
260
+ <UserRow
261
+ id={item.id}
262
+ name={item.name}
263
+ isActive={item.isActive}
264
+ />
265
+ )}
266
+
267
+ const UserRow = memo(function UserRow({ id, name, isActive }: Props) {
268
+ // Good: derive style inside memoized component
269
+ const backgroundColor = isActive ? 'green' : 'gray'
270
+ return <View style={[styles.row, { backgroundColor }]}>{/* ... */}</View>
271
+ })
272
+ ```
273
+
274
+ **Correct: hoist static styles in module scope**
275
+
276
+ ```tsx
277
+ const activeStyle = { backgroundColor: 'green' }
278
+ const inactiveStyle = { backgroundColor: 'gray' }
279
+
280
+ renderItem={({ item }) => (
281
+ <UserRow
282
+ name={item.name}
283
+ // Good: stable references
284
+ style={item.isActive ? activeStyle : inactiveStyle}
285
+ />
286
+ )}
287
+ ```
288
+
289
+ Passing primitives or stable references allows `memo()` to skip re-renders when
290
+
291
+ the actual values haven't changed.
292
+
293
+ **Note:** If you have the React Compiler enabled, it handles memoization
294
+
295
+ automatically and these manual optimizations become less critical.
296
+
297
+ ### 2.2 Hoist callbacks to the root of lists
298
+
299
+ **Impact: MEDIUM (Fewer re-renders and faster lists)**
300
+
301
+ When passing callback functions to list items, create a single instance of the
302
+
303
+ callback at the root of the list. Items should then call it with a unique
304
+
305
+ identifier.
306
+
307
+ **Incorrect: creates a new callback on each render**
308
+
309
+ ```typescript
310
+ return (
311
+ <LegendList
312
+ renderItem={({ item }) => {
313
+ // bad: creates a new callback on each render
314
+ const onPress = () => handlePress(item.id)
315
+ return <Item key={item.id} item={item} onPress={onPress} />
316
+ }}
317
+ />
318
+ )
319
+ ```
320
+
321
+ **Correct: a single function instance passed to each item**
322
+
323
+ ```typescript
324
+ const onPress = useCallback(() => handlePress(item.id), [handlePress, item.id])
325
+
326
+ return (
327
+ <LegendList
328
+ renderItem={({ item }) => (
329
+ <Item key={item.id} item={item} onPress={onPress} />
330
+ )}
331
+ />
332
+ )
333
+ ```
334
+
335
+ Reference: [https://example.com](https://example.com)
336
+
337
+ ### 2.3 Keep List Items Lightweight
338
+
339
+ **Impact: HIGH (reduces render time for visible items during scroll)**
340
+
341
+ List items should be as inexpensive as possible to render. Minimize hooks, avoid
342
+
343
+ queries, and limit React Context access. Virtualized lists render many items
344
+
345
+ during scroll—expensive items cause jank.
346
+
347
+ **Incorrect: heavy list item**
348
+
349
+ ```tsx
350
+ function ProductRow({ id }: { id: string }) {
351
+ // Bad: query inside list item
352
+ const { data: product } = useQuery(['product', id], () => fetchProduct(id))
353
+ // Bad: multiple context accesses
354
+ const theme = useContext(ThemeContext)
355
+ const user = useContext(UserContext)
356
+ const cart = useContext(CartContext)
357
+ // Bad: expensive computation
358
+ const recommendations = useMemo(
359
+ () => computeRecommendations(product),
360
+ [product]
361
+ )
362
+
363
+ return <View>{/* ... */}</View>
364
+ }
365
+ ```
366
+
367
+ **Correct: lightweight list item**
368
+
369
+ ```tsx
370
+ function ProductRow({ name, price, imageUrl }: Props) {
371
+ // Good: receives only primitives, minimal hooks
372
+ return (
373
+ <View>
374
+ <Image source={{ uri: imageUrl }} />
375
+ <Text>{name}</Text>
376
+ <Text>{price}</Text>
377
+ </View>
378
+ )
379
+ }
380
+ ```
381
+
382
+ **Move data fetching to parent:**
383
+
384
+ ```tsx
385
+ // Parent fetches all data once
386
+ function ProductList() {
387
+ const { data: products } = useQuery(['products'], fetchProducts)
388
+
389
+ return (
390
+ <LegendList
391
+ data={products}
392
+ renderItem={({ item }) => (
393
+ <ProductRow name={item.name} price={item.price} imageUrl={item.image} />
394
+ )}
395
+ />
396
+ )
397
+ }
398
+ ```
399
+
400
+ **For shared values, use Zustand selectors instead of Context:**
401
+
402
+ ```tsx
403
+ // Incorrect: Context causes re-render when any cart value changes
404
+ function ProductRow({ id, name }: Props) {
405
+ const { items } = useContext(CartContext)
406
+ const inCart = items.includes(id)
407
+ // ...
408
+ }
409
+
410
+ // Correct: Zustand selector only re-renders when this specific value changes
411
+ function 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
+ ```
417
+
418
+ **Guidelines for list items:**
419
+
420
+ - No queries or data fetching
421
+
422
+ - No expensive computations (move to parent or memoize at parent level)
423
+
424
+ - Prefer Zustand selectors over React Context
425
+
426
+ - Minimize useState/useEffect hooks
427
+
428
+ - Pass pre-computed values as props
429
+
430
+ The goal: list items should be simple rendering functions that take props and
431
+
432
+ return JSX.
433
+
434
+ ### 2.4 Optimize List Performance with Stable Object References
435
+
436
+ **Impact: CRITICAL (virtualization relies on reference stability)**
437
+
438
+ Don't map or filter data before passing to virtualized lists. Virtualization
439
+
440
+ relies on object reference stability to know what changed—new references cause
441
+
442
+ full re-renders of all visible items. Attempt to prevent frequent renders at the
443
+
444
+ list-parent level.
445
+
446
+ Where needed, use context selectors within list items.
447
+
448
+ **Incorrect: creates new object references on every keystroke**
449
+
450
+ ```tsx
451
+ function DomainSearch() {
452
+ const { keyword, setKeyword } = useKeywordZustandState()
453
+ const { data: tlds } = useTlds()
454
+
455
+ // Bad: creates new objects on every render, reparenting the entire list on every keystroke
456
+ const domains = tlds.map((tld) => ({
457
+ domain: `${keyword}.${tld.name}`,
458
+ tld: tld.name,
459
+ price: tld.price,
460
+ }))
461
+
462
+ return (
463
+ <>
464
+ <TextInput value={keyword} onChangeText={setKeyword} />
465
+ <LegendList
466
+ data={domains}
467
+ renderItem={({ item }) => <DomainItem item={item} keyword={keyword} />}
468
+ />
469
+ </>
470
+ )
471
+ }
472
+ ```
473
+
474
+ **Correct: stable references, transform inside items**
475
+
476
+ ```tsx
477
+ const renderItem = ({ item }) => <DomainItem tld={item} />
478
+
479
+ function DomainSearch() {
480
+ const { data: tlds } = useTlds()
481
+
482
+ return (
483
+ <LegendList
484
+ // good: as long as the data is stable, LegendList will not re-render the entire list
485
+ data={tlds}
486
+ renderItem={renderItem}
487
+ />
488
+ )
489
+ }
490
+
491
+ function DomainItem({ tld }: { tld: Tld }) {
492
+ // good: transform within items, and don't pass the dynamic data as a prop
493
+ // good: use a selector function from zustand to receive a stable string back
494
+ const domain = useKeywordZustandState((s) => s.keyword + '.' + tld.name)
495
+ return <Text>{domain}</Text>
496
+ }
497
+ ```
498
+
499
+ **Updating parent array reference:**
500
+
501
+ ```tsx
502
+ // good: creates a new array instance without mutating the inner objects
503
+ // good: parent array reference is unaffected by typing and updating "keyword"
504
+ const sortedTlds = tlds.toSorted((a, b) => a.name.localeCompare(b.name))
505
+
506
+ return <LegendList data={sortedTlds} renderItem={renderItem} />
507
+ ```
508
+
509
+ Creating a new array instance can be okay, as long as its inner object
510
+
511
+ references are stable. For instance, if you sort a list of objects:
512
+
513
+ Even though this creates a new array instance `sortedTlds`, the inner object
514
+
515
+ references are stable.
516
+
517
+ **With zustand for dynamic data: avoids parent re-renders**
518
+
519
+ ```tsx
520
+ function DomainItemFavoriteButton({ tld }: { tld: Tld }) {
521
+ const isFavorited = useFavoritesStore((s) => s.favorites.has(tld.id))
522
+ return <TldFavoriteButton isFavorited={isFavorited} />
523
+ }
524
+ ```
525
+
526
+ Virtualization can now skip items that haven't changed when typing. Only visible
527
+
528
+ items (~20) re-render on keystroke, rather than the parent.
529
+
530
+ **Deriving state within list items based on parent data (avoids parent
531
+
532
+ re-renders):**
533
+
534
+ For components where the data is conditional based on the parent state, this
535
+
536
+ pattern is even more important. For example, if you are checking if an item is
537
+
538
+ favorited, toggling favorites only re-renders one component if the item itself
539
+
540
+ is in charge of accessing the state rather than the parent:
541
+
542
+ Note: if you're using the React Compiler, you can read React Context values
543
+
544
+ directly within list items. Although this is slightly slower than using a
545
+
546
+ Zustand selector in most cases, the effect may be negligible.
547
+
548
+ ### 2.5 Pass Primitives to List Items for Memoization
549
+
550
+ **Impact: HIGH (enables effective memo() comparison)**
551
+
552
+ When possible, pass only primitive values (strings, numbers, booleans) as props
553
+
554
+ to list item components. Primitives enable shallow comparison in `memo()` to
555
+
556
+ work correctly, skipping re-renders when values haven't changed.
557
+
558
+ **Incorrect: object prop requires deep comparison**
559
+
560
+ ```tsx
561
+ type User = { id: string; name: string; email: string; avatar: string }
562
+
563
+ const UserRow = memo(function UserRow({ user }: { user: User }) {
564
+ // memo() compares user by reference, not value
565
+ // If parent creates new user object, this re-renders even if data is same
566
+ return <Text>{user.name}</Text>
567
+ })
568
+
569
+ renderItem={({ item }) => <UserRow user={item} />}
570
+ ```
571
+
572
+ This can still be optimized, but it is harder to memoize properly.
573
+
574
+ **Correct: primitive props enable shallow comparison**
575
+
576
+ ```tsx
577
+ const UserRow = memo(function UserRow({
578
+ id,
579
+ name,
580
+ email,
581
+ }: {
582
+ id: string
583
+ name: string
584
+ email: string
585
+ }) {
586
+ // memo() compares each primitive directly
587
+ // Re-renders only if id, name, or email actually changed
588
+ return <Text>{name}</Text>
589
+ })
590
+
591
+ renderItem={({ item }) => (
592
+ <UserRow id={item.id} name={item.name} email={item.email} />
593
+ )}
594
+ ```
595
+
596
+ **Pass only what you need:**
597
+
598
+ ```tsx
599
+ // Incorrect: passing entire item when you only need name
600
+ <UserRow user={item} />
601
+
602
+ // Correct: pass only the fields the component uses
603
+ <UserRow name={item.name} avatarUrl={item.avatar} />
604
+ ```
605
+
606
+ **For callbacks, hoist or use item ID:**
607
+
608
+ ```tsx
609
+ // Incorrect: inline function creates new reference
610
+ <UserRow name={item.name} onPress={() => handlePress(item.id)} />
611
+
612
+ // Correct: pass ID, handle in child
613
+ <UserRow id={item.id} name={item.name} />
614
+
615
+ const UserRow = memo(function UserRow({ id, name }: Props) {
616
+ const handlePress = useCallback(() => {
617
+ // use id here
618
+ }, [id])
619
+ return <Pressable onPress={handlePress}><Text>{name}</Text></Pressable>
620
+ })
621
+ ```
622
+
623
+ Primitive props make memoization predictable and effective.
624
+
625
+ **Note:** If you have the React Compiler enabled, you do not need to use
626
+
627
+ `memo()` or `useCallback()`, but the object references still apply.
628
+
629
+ ### 2.6 Use a List Virtualizer for Any List
630
+
631
+ **Impact: HIGH (reduced memory, faster mounts)**
632
+
633
+ Use a list virtualizer like LegendList or FlashList instead of ScrollView with
634
+
635
+ mapped children—even for short lists. Virtualizers only render visible items,
636
+
637
+ reducing memory usage and mount time. ScrollView renders all children upfront,
638
+
639
+ which gets expensive quickly.
640
+
641
+ **Incorrect: ScrollView renders all items at once**
642
+
643
+ ```tsx
644
+ function 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 visible
654
+ ```
655
+
656
+ **Correct: virtualizer renders only visible items**
657
+
658
+ ```tsx
659
+ import { LegendList } from '@legendapp/list'
660
+
661
+ function Feed({ items }: { items: Item[] }) {
662
+ return (
663
+ <LegendList
664
+ data={items}
665
+ // if you aren't using React Compiler, wrap these with useCallback
666
+ 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 time
673
+ ```
674
+
675
+ **Alternative: FlashList**
676
+
677
+ ```tsx
678
+ import { FlashList } from '@shopify/flash-list'
679
+
680
+ function Feed({ items }: { items: Item[] }) {
681
+ return (
682
+ <FlashList
683
+ data={items}
684
+ // if you aren't using React Compiler, wrap these with useCallback
685
+ renderItem={({ item }) => <ItemCard item={item} />}
686
+ keyExtractor={(item) => item.id}
687
+ />
688
+ )
689
+ }
690
+ ```
691
+
692
+ Benefits apply to any screen with scrollable content—profiles, settings, feeds,
693
+
694
+ search results. Default to virtualization.
695
+
696
+ ### 2.7 Use Compressed Images in Lists
697
+
698
+ **Impact: HIGH (faster load times, less memory)**
699
+
700
+ Always load compressed, appropriately-sized images in lists. Full-resolution
701
+
702
+ images consume excessive memory and cause scroll jank. Request thumbnails from
703
+
704
+ your server or use an image CDN with resize parameters.
705
+
706
+ **Incorrect: full-resolution images**
707
+
708
+ ```tsx
709
+ function ProductItem({ product }: { product: Product }) {
710
+ return (
711
+ <View>
712
+ {/* 4000x3000 image loaded for a 100x100 thumbnail */}
713
+ <Image
714
+ source={{ uri: product.imageUrl }}
715
+ style={{ width: 100, height: 100 }}
716
+ />
717
+ <Text>{product.name}</Text>
718
+ </View>
719
+ )
720
+ }
721
+ ```
722
+
723
+ **Correct: request appropriately-sized image**
724
+
725
+ ```tsx
726
+ function ProductItem({ product }: { product: Product }) {
727
+ // Request a 200x200 image (2x for retina)
728
+ const thumbnailUrl = `${product.imageUrl}?w=200&h=200&fit=cover`
729
+
730
+ return (
731
+ <View>
732
+ <Image
733
+ source={{ uri: thumbnailUrl }}
734
+ style={{ width: 100, height: 100 }}
735
+ contentFit='cover'
736
+ />
737
+ <Text>{product.name}</Text>
738
+ </View>
739
+ )
740
+ }
741
+ ```
742
+
743
+ Use an optimized image component with built-in caching and placeholder support,
744
+
745
+ such as `expo-image` or `SolitoImage` (which uses `expo-image` under the hood).
746
+
747
+ Request images at 2x the display size for retina screens.
748
+
749
+ ### 2.8 Use Item Types for Heterogeneous Lists
750
+
751
+ **Impact: HIGH (efficient recycling, less layout thrashing)**
752
+
753
+ When a list has different item layouts (messages, images, headers, etc.), use a
754
+
755
+ `type` field on each item and provide `getItemType` to the list. This puts items
756
+
757
+ into separate recycling pools so a message component never gets recycled into an
758
+
759
+ image component.
760
+
761
+ [LegendList getItemType](https://legendapp.com/open-source/list/api/props/#getitemtype-v2)
762
+
763
+ **Incorrect: single component with conditionals**
764
+
765
+ ```tsx
766
+ type Item = { id: string; text?: string; imageUrl?: string; isHeader?: boolean }
767
+
768
+ function 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
+ }
777
+
778
+ function Feed({ items }: { items: Item[] }) {
779
+ return (
780
+ <LegendList
781
+ data={items}
782
+ renderItem={({ item }) => <ListItem item={item} />}
783
+ recycleItems
784
+ />
785
+ )
786
+ }
787
+ ```
788
+
789
+ **Correct: typed items with separate components**
790
+
791
+ ```tsx
792
+ type HeaderItem = { id: string; type: 'header'; title: string }
793
+ type MessageItem = { id: string; type: 'message'; text: string }
794
+ type ImageItem = { id: string; type: 'image'; url: string }
795
+ type FeedItem = HeaderItem | MessageItem | ImageItem
796
+
797
+ function Feed({ items }: { items: FeedItem[] }) {
798
+ return (
799
+ <LegendList
800
+ 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
+ recycleItems
814
+ />
815
+ )
816
+ }
817
+ ```
818
+
819
+ **Why this matters:**
820
+
821
+ ```tsx
822
+ <LegendList
823
+ 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 48
830
+ case 'message':
831
+ return 72
832
+ case 'image':
833
+ return 300
834
+ default:
835
+ return 72
836
+ }
837
+ }}
838
+ renderItem={({ item }) => {
839
+ /* ... */
840
+ }}
841
+ recycleItems
842
+ />
843
+ ```
844
+
845
+ - **Recycling efficiency**: Items with the same type share a recycling pool
846
+
847
+ - **No layout thrashing**: A header never recycles into an image cell
848
+
849
+ - **Type safety**: TypeScript can narrow the item type in each branch
850
+
851
+ - **Better size estimation**: Use `getEstimatedItemSize` with `itemType` for
852
+
853
+ accurate estimates per type
854
+
855
+ ---
856
+
857
+ ## 3. Animation
858
+
859
+ **Impact: HIGH**
860
+
861
+ GPU-accelerated animations, Reanimated patterns, and avoiding
862
+ render thrashing during gestures.
863
+
864
+ ### 3.1 Animate Transform and Opacity Instead of Layout Properties
865
+
866
+ **Impact: HIGH (GPU-accelerated animations, no layout recalculation)**
867
+
868
+ 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.
869
+
870
+ **Incorrect: animates height, triggers layout every frame**
871
+
872
+ ```tsx
873
+ import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
874
+
875
+ function CollapsiblePanel({ expanded }: { expanded: boolean }) {
876
+ const animatedStyle = useAnimatedStyle(() => ({
877
+ height: withTiming(expanded ? 200 : 0), // triggers layout on every frame
878
+ overflow: 'hidden',
879
+ }))
880
+
881
+ return <Animated.View style={animatedStyle}>{children}</Animated.View>
882
+ }
883
+ ```
884
+
885
+ **Correct: animates scaleY, GPU-accelerated**
886
+
887
+ ```tsx
888
+ import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
889
+
890
+ function CollapsiblePanel({ expanded }: { expanded: boolean }) {
891
+ const animatedStyle = useAnimatedStyle(() => ({
892
+ transform: [
893
+ { scaleY: withTiming(expanded ? 1 : 0) },
894
+ ],
895
+ opacity: withTiming(expanded ? 1 : 0),
896
+ }))
897
+
898
+ return (
899
+ <Animated.View style={[{ height: 200, transformOrigin: 'top' }, animatedStyle]}>
900
+ {children}
901
+ </Animated.View>
902
+ )
903
+ }
904
+ ```
905
+
906
+ **Correct: animates translateY for slide animations**
907
+
908
+ ```tsx
909
+ import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
910
+
911
+ function SlideIn({ visible }: { visible: boolean }) {
912
+ const animatedStyle = useAnimatedStyle(() => ({
913
+ transform: [
914
+ { translateY: withTiming(visible ? 0 : 100) },
915
+ ],
916
+ opacity: withTiming(visible ? 1 : 0),
917
+ }))
918
+
919
+ return <Animated.View style={animatedStyle}>{children}</Animated.View>
920
+ }
921
+ ```
922
+
923
+ GPU-accelerated properties: `transform` (translate, scale, rotate), `opacity`. Everything else triggers layout.
924
+
925
+ ### 3.2 Prefer useDerivedValue Over useAnimatedReaction
926
+
927
+ **Impact: MEDIUM (cleaner code, automatic dependency tracking)**
928
+
929
+ When deriving a shared value from another, use `useDerivedValue` instead of
930
+
931
+ `useAnimatedReaction`. Derived values are declarative, automatically track
932
+
933
+ dependencies, and return a value you can use directly. Animated reactions are
934
+
935
+ for side effects, not derivations.
936
+
937
+ [Reanimated useDerivedValue](https://docs.swmansion.com/react-native-reanimated/docs/core/useDerivedValue)
938
+
939
+ **Incorrect: useAnimatedReaction for derivation**
940
+
941
+ ```tsx
942
+ import { useSharedValue, useAnimatedReaction } from 'react-native-reanimated'
943
+
944
+ function MyComponent() {
945
+ const progress = useSharedValue(0)
946
+ const opacity = useSharedValue(1)
947
+
948
+ useAnimatedReaction(
949
+ () => progress.value,
950
+ (current) => {
951
+ opacity.value = 1 - current
952
+ }
953
+ )
954
+
955
+ // ...
956
+ }
957
+ ```
958
+
959
+ **Correct: useDerivedValue**
960
+
961
+ ```tsx
962
+ import { useSharedValue, useDerivedValue } from 'react-native-reanimated'
963
+
964
+ function MyComponent() {
965
+ const progress = useSharedValue(0)
966
+
967
+ const opacity = useDerivedValue(() => 1 - progress.get())
968
+
969
+ // ...
970
+ }
971
+ ```
972
+
973
+ Use `useAnimatedReaction` only for side effects that don't produce a value
974
+
975
+ (e.g., triggering haptics, logging, calling `runOnJS`).
976
+
977
+ ### 3.3 Use GestureDetector for Animated Press States
978
+
979
+ **Impact: MEDIUM (UI thread animations, smoother press feedback)**
980
+
981
+ For animated press states (scale, opacity on press), use `GestureDetector` with
982
+
983
+ `Gesture.Tap()` and shared values instead of Pressable's
984
+
985
+ `onPressIn`/`onPressOut`. Gesture callbacks run on the UI thread as worklets—no
986
+
987
+ JS thread round-trip for press animations.
988
+
989
+ [Gesture Handler Tap Gesture](https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture)
990
+
991
+ **Incorrect: Pressable with JS thread callbacks**
992
+
993
+ ```tsx
994
+ import { Pressable } from 'react-native'
995
+ import Animated, {
996
+ useSharedValue,
997
+ useAnimatedStyle,
998
+ withTiming,
999
+ } from 'react-native-reanimated'
1000
+
1001
+ function AnimatedButton({ onPress }: { onPress: () => void }) {
1002
+ const scale = useSharedValue(1)
1003
+
1004
+ const animatedStyle = useAnimatedStyle(() => ({
1005
+ transform: [{ scale: scale.value }],
1006
+ }))
1007
+
1008
+ return (
1009
+ <Pressable
1010
+ 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
+ ```
1021
+
1022
+ **Correct: GestureDetector with UI thread worklets**
1023
+
1024
+ ```tsx
1025
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler'
1026
+ import Animated, {
1027
+ useSharedValue,
1028
+ useAnimatedStyle,
1029
+ withTiming,
1030
+ interpolate,
1031
+ runOnJS,
1032
+ } from 'react-native-reanimated'
1033
+
1034
+ function AnimatedButton({ onPress }: { onPress: () => void }) {
1035
+ // Store the press STATE (0 = not pressed, 1 = pressed)
1036
+ const pressed = useSharedValue(0)
1037
+
1038
+ 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
+ })
1048
+
1049
+ // Derive visual values from the state
1050
+ const animatedStyle = useAnimatedStyle(() => ({
1051
+ transform: [
1052
+ { scale: interpolate(withTiming(pressed.get()), [0, 1], [1, 0.95]) },
1053
+ ],
1054
+ }))
1055
+
1056
+ return (
1057
+ <GestureDetector gesture={tap}>
1058
+ <Animated.View style={animatedStyle}>
1059
+ <Text>Press me</Text>
1060
+ </Animated.View>
1061
+ </GestureDetector>
1062
+ )
1063
+ }
1064
+ ```
1065
+
1066
+ Store the press **state** (0 or 1), then derive the scale via `interpolate`.
1067
+
1068
+ This keeps the shared value as ground truth. Use `runOnJS` to call JS functions
1069
+
1070
+ from worklets. Use `.set()` and `.get()` for React Compiler compatibility.
1071
+
1072
+ ---
1073
+
1074
+ ## 4. Scroll Performance
1075
+
1076
+ **Impact: HIGH**
1077
+
1078
+ Tracking scroll position without causing render thrashing.
1079
+
1080
+ ### 4.1 Never Track Scroll Position in useState
1081
+
1082
+ **Impact: HIGH (prevents render thrashing during scroll)**
1083
+
1084
+ Never store scroll position in `useState`. Scroll events fire rapidly—state
1085
+
1086
+ updates cause render thrashing and dropped frames. Use a Reanimated shared value
1087
+
1088
+ for animations or a ref for non-reactive tracking.
1089
+
1090
+ **Incorrect: useState causes jank**
1091
+
1092
+ ```tsx
1093
+ import { useState } from 'react'
1094
+ import {
1095
+ ScrollView,
1096
+ NativeSyntheticEvent,
1097
+ NativeScrollEvent,
1098
+ } from 'react-native'
1099
+
1100
+ function Feed() {
1101
+ const [scrollY, setScrollY] = useState(0)
1102
+
1103
+ const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
1104
+ setScrollY(e.nativeEvent.contentOffset.y) // re-renders on every frame
1105
+ }
1106
+
1107
+ return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />
1108
+ }
1109
+ ```
1110
+
1111
+ **Correct: Reanimated for animations**
1112
+
1113
+ ```tsx
1114
+ import Animated, {
1115
+ useSharedValue,
1116
+ useAnimatedScrollHandler,
1117
+ } from 'react-native-reanimated'
1118
+
1119
+ function Feed() {
1120
+ const scrollY = useSharedValue(0)
1121
+
1122
+ const onScroll = useAnimatedScrollHandler({
1123
+ onScroll: (e) => {
1124
+ scrollY.value = e.contentOffset.y // runs on UI thread, no re-render
1125
+ },
1126
+ })
1127
+
1128
+ return (
1129
+ <Animated.ScrollView
1130
+ 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
+ ```
1138
+
1139
+ **Correct: ref for non-reactive tracking**
1140
+
1141
+ ```tsx
1142
+ import { useRef } from 'react'
1143
+ import {
1144
+ ScrollView,
1145
+ NativeSyntheticEvent,
1146
+ NativeScrollEvent,
1147
+ } from 'react-native'
1148
+
1149
+ function Feed() {
1150
+ const scrollY = useRef(0)
1151
+
1152
+ const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
1153
+ scrollY.current = e.nativeEvent.contentOffset.y // no re-render
1154
+ }
1155
+
1156
+ return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />
1157
+ }
1158
+ ```
1159
+
1160
+ ---
1161
+
1162
+ ## 5. Navigation
1163
+
1164
+ **Impact: HIGH**
1165
+
1166
+ Using native navigators for stack and tab navigation instead of
1167
+ JS-based alternatives.
1168
+
1169
+ ### 5.1 Use Native Navigators for Navigation
1170
+
1171
+ **Impact: HIGH (native performance, platform-appropriate UI)**
1172
+
1173
+ Always use native navigators instead of JS-based ones. Native navigators use
1174
+
1175
+ platform APIs (UINavigationController on iOS, Fragment on Android) for better
1176
+
1177
+ performance and native behavior.
1178
+
1179
+ **For stacks:** Use `@react-navigation/native-stack` or expo-router's default
1180
+
1181
+ stack (which uses native-stack). Avoid `@react-navigation/stack`.
1182
+
1183
+ **For tabs:** Use `react-native-bottom-tabs` (native) or expo-router's native
1184
+
1185
+ tabs. Avoid `@react-navigation/bottom-tabs` when native feel matters.
1186
+
1187
+ - [React Navigation Native Stack](https://reactnavigation.org/docs/native-stack-navigator)
1188
+
1189
+ - [React Native Bottom Tabs with React Navigation](https://oss.callstack.com/react-native-bottom-tabs/docs/guides/usage-with-react-navigation)
1190
+
1191
+ - [React Native Bottom Tabs with Expo Router](https://oss.callstack.com/react-native-bottom-tabs/docs/guides/usage-with-expo-router)
1192
+
1193
+ - [Expo Router Native Tabs](https://docs.expo.dev/router/advanced/native-tabs)
1194
+
1195
+ **Incorrect: JS stack navigator**
1196
+
1197
+ ```tsx
1198
+ import { createStackNavigator } from '@react-navigation/stack'
1199
+
1200
+ const Stack = createStackNavigator()
1201
+
1202
+ function 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
+ ```
1211
+
1212
+ **Correct: native stack with react-navigation**
1213
+
1214
+ ```tsx
1215
+ import { createNativeStackNavigator } from '@react-navigation/native-stack'
1216
+
1217
+ const Stack = createNativeStackNavigator()
1218
+
1219
+ function 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
+ ```
1228
+
1229
+ **Correct: expo-router uses native stack by default**
1230
+
1231
+ ```tsx
1232
+ // app/_layout.tsx
1233
+ import { Stack } from 'expo-router'
1234
+
1235
+ export default function Layout() {
1236
+ return <Stack />
1237
+ }
1238
+ ```
1239
+
1240
+ **Incorrect: JS bottom tabs**
1241
+
1242
+ ```tsx
1243
+ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
1244
+
1245
+ const Tab = createBottomTabNavigator()
1246
+
1247
+ function 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
+ ```
1256
+
1257
+ **Correct: native bottom tabs with react-navigation**
1258
+
1259
+ ```tsx
1260
+ import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation'
1261
+
1262
+ const Tab = createNativeBottomTabNavigator()
1263
+
1264
+ function App() {
1265
+ return (
1266
+ <Tab.Navigator>
1267
+ <Tab.Screen
1268
+ name='Home'
1269
+ component={HomeScreen}
1270
+ options={{
1271
+ tabBarIcon: () => ({ sfSymbol: 'house' }),
1272
+ }}
1273
+ />
1274
+ <Tab.Screen
1275
+ name='Settings'
1276
+ component={SettingsScreen}
1277
+ options={{
1278
+ tabBarIcon: () => ({ sfSymbol: 'gear' }),
1279
+ }}
1280
+ />
1281
+ </Tab.Navigator>
1282
+ )
1283
+ }
1284
+ ```
1285
+
1286
+ **Correct: expo-router native tabs**
1287
+
1288
+ ```tsx
1289
+ // app/(tabs)/_layout.tsx
1290
+ import { NativeTabs } from 'expo-router/unstable-native-tabs'
1291
+
1292
+ export 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
+ ```
1307
+
1308
+ On iOS, native tabs automatically enable `contentInsetAdjustmentBehavior` on the
1309
+
1310
+ first `ScrollView` at the root of each tab screen, so content scrolls correctly
1311
+
1312
+ behind the translucent tab bar. If you need to disable this, use
1313
+
1314
+ `disableAutomaticContentInsets` on the trigger.
1315
+
1316
+ **Incorrect: custom header component**
1317
+
1318
+ ```tsx
1319
+ <Stack.Screen
1320
+ name='Profile'
1321
+ component={ProfileScreen}
1322
+ options={{
1323
+ header: () => <CustomHeader title='Profile' />,
1324
+ }}
1325
+ />
1326
+ ```
1327
+
1328
+ **Correct: native header options**
1329
+
1330
+ ```tsx
1331
+ <Stack.Screen
1332
+ name='Profile'
1333
+ component={ProfileScreen}
1334
+ options={{
1335
+ title: 'Profile',
1336
+ headerLargeTitleEnabled: true,
1337
+ headerSearchBarOptions: {
1338
+ placeholder: 'Search',
1339
+ },
1340
+ }}
1341
+ />
1342
+ ```
1343
+
1344
+ Native headers support iOS large titles, search bars, blur effects, and proper
1345
+
1346
+ safe area handling automatically.
1347
+
1348
+ - **Performance**: Native transitions and gestures run on the UI thread
1349
+
1350
+ - **Platform behavior**: Automatic iOS large titles, Android material design
1351
+
1352
+ - **System integration**: Scroll-to-top on tab tap, PiP avoidance, proper safe
1353
+
1354
+ areas
1355
+
1356
+ - **Accessibility**: Platform accessibility features work automatically
1357
+
1358
+ ---
1359
+
1360
+ ## 6. React State
1361
+
1362
+ **Impact: MEDIUM**
1363
+
1364
+ Patterns for managing React state to avoid stale closures and
1365
+ unnecessary re-renders.
1366
+
1367
+ ### 6.1 Minimize State Variables and Derive Values
1368
+
1369
+ **Impact: MEDIUM (fewer re-renders, less state drift)**
1370
+
1371
+ 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.
1372
+
1373
+ **Incorrect: redundant state**
1374
+
1375
+ ```tsx
1376
+ function Cart({ items }: { items: Item[] }) {
1377
+ const [total, setTotal] = useState(0)
1378
+ const [itemCount, setItemCount] = useState(0)
1379
+
1380
+ useEffect(() => {
1381
+ setTotal(items.reduce((sum, item) => sum + item.price, 0))
1382
+ setItemCount(items.length)
1383
+ }, [items])
1384
+
1385
+ return (
1386
+ <View>
1387
+ <Text>{itemCount} items</Text>
1388
+ <Text>Total: ${total}</Text>
1389
+ </View>
1390
+ )
1391
+ }
1392
+ ```
1393
+
1394
+ **Correct: derived values**
1395
+
1396
+ ```tsx
1397
+ function Cart({ items }: { items: Item[] }) {
1398
+ const total = items.reduce((sum, item) => sum + item.price, 0)
1399
+ const itemCount = items.length
1400
+
1401
+ return (
1402
+ <View>
1403
+ <Text>{itemCount} items</Text>
1404
+ <Text>Total: ${total}</Text>
1405
+ </View>
1406
+ )
1407
+ }
1408
+ ```
1409
+
1410
+ **Another example:**
1411
+
1412
+ ```tsx
1413
+ // Incorrect: storing both firstName, lastName, AND fullName
1414
+ const [firstName, setFirstName] = useState('')
1415
+ const [lastName, setLastName] = useState('')
1416
+ const [fullName, setFullName] = useState('')
1417
+
1418
+ // Correct: derive fullName
1419
+ const [firstName, setFirstName] = useState('')
1420
+ const [lastName, setLastName] = useState('')
1421
+ const fullName = `${firstName} ${lastName}`
1422
+ ```
1423
+
1424
+ State should be the minimal source of truth. Everything else is derived.
1425
+
1426
+ Reference: [https://react.dev/learn/choosing-the-state-structure](https://react.dev/learn/choosing-the-state-structure)
1427
+
1428
+ ### 6.2 Use fallback state instead of initialState
1429
+
1430
+ **Impact: MEDIUM (reactive fallbacks without syncing)**
1431
+
1432
+ Use `undefined` as initial state and nullish coalescing (`??`) to fall back to
1433
+
1434
+ parent or server values. State represents user intent only—`undefined` means
1435
+
1436
+ "user hasn't chosen yet." This enables reactive fallbacks that update when the
1437
+
1438
+ source changes, not just on initial render.
1439
+
1440
+ **Incorrect: syncs state, loses reactivity**
1441
+
1442
+ ```tsx
1443
+ type Props = { fallbackEnabled: boolean }
1444
+
1445
+ function Toggle({ fallbackEnabled }: Props) {
1446
+ const [enabled, setEnabled] = useState(defaultEnabled)
1447
+ // If fallbackEnabled changes, state is stale
1448
+ // State mixes user intent with default value
1449
+
1450
+ return <Switch value={enabled} onValueChange={setEnabled} />
1451
+ }
1452
+ ```
1453
+
1454
+ **Correct: state is user intent, reactive fallback**
1455
+
1456
+ ```tsx
1457
+ type Props = { fallbackEnabled: boolean }
1458
+
1459
+ function Toggle({ fallbackEnabled }: Props) {
1460
+ const [_enabled, setEnabled] = useState<boolean | undefined>(undefined)
1461
+ const enabled = _enabled ?? defaultEnabled
1462
+ // undefined = user hasn't touched it, falls back to prop
1463
+ // If defaultEnabled changes, component reflects it
1464
+ // Once user interacts, their choice persists
1465
+
1466
+ return <Switch value={enabled} onValueChange={setEnabled} />
1467
+ }
1468
+ ```
1469
+
1470
+ **With server data:**
1471
+
1472
+ ```tsx
1473
+ function ProfileForm({ data }: { data: User }) {
1474
+ const [_theme, setTheme] = useState<string | undefined>(undefined)
1475
+ const theme = _theme ?? data.theme
1476
+ // Shows server value until user overrides
1477
+ // Server refetch updates the fallback automatically
1478
+
1479
+ return <ThemePicker value={theme} onChange={setTheme} />
1480
+ }
1481
+ ```
1482
+
1483
+ ### 6.3 useState Dispatch updaters for State That Depends on Current Value
1484
+
1485
+ **Impact: MEDIUM (avoids stale closures, prevents unnecessary re-renders)**
1486
+
1487
+ When the next state depends on the current state, use a dispatch updater
1488
+
1489
+ (`setState(prev => ...)`) instead of reading the state variable directly in a
1490
+
1491
+ callback. This avoids stale closures and ensures you're comparing against the
1492
+
1493
+ latest value.
1494
+
1495
+ **Incorrect: reads state directly**
1496
+
1497
+ ```tsx
1498
+ const [size, setSize] = useState<Size | undefined>(undefined)
1499
+
1500
+ const onLayout = (e: LayoutChangeEvent) => {
1501
+ const { width, height } = e.nativeEvent.layout
1502
+ // size may be stale in this closure
1503
+ if (size?.width !== width || size?.height !== height) {
1504
+ setSize({ width, height })
1505
+ }
1506
+ }
1507
+ ```
1508
+
1509
+ **Correct: dispatch updater**
1510
+
1511
+ ```tsx
1512
+ const [size, setSize] = useState<Size | undefined>(undefined)
1513
+
1514
+ const onLayout = (e: LayoutChangeEvent) => {
1515
+ const { width, height } = e.nativeEvent.layout
1516
+ setSize((prev) => {
1517
+ if (prev?.width === width && prev?.height === height) return prev
1518
+ return { width, height }
1519
+ })
1520
+ }
1521
+ ```
1522
+
1523
+ Returning the previous value from the updater skips the re-render.
1524
+
1525
+ For primitive states, you don't need to compare values before firing a
1526
+
1527
+ re-render.
1528
+
1529
+ **Incorrect: unnecessary comparison for primitive state**
1530
+
1531
+ ```tsx
1532
+ const [size, setSize] = useState<Size | undefined>(undefined)
1533
+
1534
+ const onLayout = (e: LayoutChangeEvent) => {
1535
+ const { width, height } = e.nativeEvent.layout
1536
+ setSize((prev) => (prev === width ? prev : width))
1537
+ }
1538
+ ```
1539
+
1540
+ **Correct: sets primitive state directly**
1541
+
1542
+ ```tsx
1543
+ const [size, setSize] = useState<Size | undefined>(undefined)
1544
+
1545
+ const onLayout = (e: LayoutChangeEvent) => {
1546
+ const { width, height } = e.nativeEvent.layout
1547
+ setSize(width)
1548
+ }
1549
+ ```
1550
+
1551
+ However, if the next state depends on the current state, you should still use a
1552
+
1553
+ dispatch updater.
1554
+
1555
+ **Incorrect: reads state directly from the callback**
1556
+
1557
+ ```tsx
1558
+ const [count, setCount] = useState(0)
1559
+
1560
+ const onTap = () => {
1561
+ setCount(count + 1)
1562
+ }
1563
+ ```
1564
+
1565
+ **Correct: dispatch updater**
1566
+
1567
+ ```tsx
1568
+ const [count, setCount] = useState(0)
1569
+
1570
+ const onTap = () => {
1571
+ setCount((prev) => prev + 1)
1572
+ }
1573
+ ```
1574
+
1575
+ ---
1576
+
1577
+ ## 7. State Architecture
1578
+
1579
+ **Impact: MEDIUM**
1580
+
1581
+ Ground truth principles for state variables and derived values.
1582
+
1583
+ ### 7.1 State Must Represent Ground Truth
1584
+
1585
+ **Impact: HIGH (cleaner logic, easier debugging, single source of truth)**
1586
+
1587
+ State variables—both React `useState` and Reanimated shared values—should
1588
+
1589
+ represent the actual state of something (e.g., `pressed`, `progress`, `isOpen`),
1590
+
1591
+ not derived visual values (e.g., `scale`, `opacity`, `translateY`). Derive
1592
+
1593
+ visual values from state using computation or interpolation.
1594
+
1595
+ **Incorrect: storing the visual output**
1596
+
1597
+ ```tsx
1598
+ const scale = useSharedValue(1)
1599
+
1600
+ const tap = Gesture.Tap()
1601
+ .onBegin(() => {
1602
+ scale.set(withTiming(0.95))
1603
+ })
1604
+ .onFinalize(() => {
1605
+ scale.set(withTiming(1))
1606
+ })
1607
+
1608
+ const animatedStyle = useAnimatedStyle(() => ({
1609
+ transform: [{ scale: scale.get() }],
1610
+ }))
1611
+ ```
1612
+
1613
+ **Correct: storing the state, deriving the visual**
1614
+
1615
+ ```tsx
1616
+ const pressed = useSharedValue(0) // 0 = not pressed, 1 = pressed
1617
+
1618
+ const tap = Gesture.Tap()
1619
+ .onBegin(() => {
1620
+ pressed.set(withTiming(1))
1621
+ })
1622
+ .onFinalize(() => {
1623
+ pressed.set(withTiming(0))
1624
+ })
1625
+
1626
+ const animatedStyle = useAnimatedStyle(() => ({
1627
+ transform: [{ scale: interpolate(pressed.get(), [0, 1], [1, 0.95]) }],
1628
+ }))
1629
+ ```
1630
+
1631
+ **Why this matters:**
1632
+
1633
+ State variables should represent real "state", not necessarily a desired end
1634
+
1635
+ result.
1636
+
1637
+ 1. **Single source of truth** — The state (`pressed`) describes what's
1638
+
1639
+ happening; visuals are derived
1640
+
1641
+ 2. **Easier to extend** — Adding opacity, rotation, or other effects just
1642
+
1643
+ requires more interpolations from the same state
1644
+
1645
+ 3. **Debugging** — Inspecting `pressed = 1` is clearer than `scale = 0.95`
1646
+
1647
+ 4. **Reusable logic** — The same `pressed` value can drive multiple visual
1648
+
1649
+ properties
1650
+
1651
+ **Same principle for React state:**
1652
+
1653
+ ```tsx
1654
+ // Incorrect: storing derived values
1655
+ const [isExpanded, setIsExpanded] = useState(false)
1656
+ const [height, setHeight] = useState(0)
1657
+
1658
+ useEffect(() => {
1659
+ setHeight(isExpanded ? 200 : 0)
1660
+ }, [isExpanded])
1661
+
1662
+ // Correct: derive from state
1663
+ const [isExpanded, setIsExpanded] = useState(false)
1664
+ const height = isExpanded ? 200 : 0
1665
+ ```
1666
+
1667
+ State is the minimal truth. Everything else is derived.
1668
+
1669
+ ---
1670
+
1671
+ ## 8. React Compiler
1672
+
1673
+ **Impact: MEDIUM**
1674
+
1675
+ Compatibility patterns for React Compiler with React Native and
1676
+ Reanimated.
1677
+
1678
+ ### 8.1 Destructure Functions Early in Render (React Compiler)
1679
+
1680
+ **Impact: HIGH (stable references, fewer re-renders)**
1681
+
1682
+ This rule is only applicable if you are using the React Compiler.
1683
+
1684
+ Destructure functions from hooks at the top of render scope. Never dot into
1685
+
1686
+ objects to call functions. Destructured functions are stable references; dotting
1687
+
1688
+ creates new references and breaks memoization.
1689
+
1690
+ **Incorrect: dotting into object**
1691
+
1692
+ ```tsx
1693
+ import { useRouter } from 'expo-router'
1694
+
1695
+ function SaveButton(props) {
1696
+ const router = useRouter()
1697
+
1698
+ // bad: react-compiler will key the cache on "props" and "router", which are objects that change each render
1699
+ const handlePress = () => {
1700
+ props.onSave()
1701
+ router.push('/success') // unstable reference
1702
+ }
1703
+
1704
+ return <Button onPress={handlePress}>Save</Button>
1705
+ }
1706
+ ```
1707
+
1708
+ **Correct: destructure early**
1709
+
1710
+ ```tsx
1711
+ import { useRouter } from 'expo-router'
1712
+
1713
+ function SaveButton({ onSave }) {
1714
+ const { push } = useRouter()
1715
+
1716
+ // good: react-compiler will key on push and onSave
1717
+ const handlePress = () => {
1718
+ onSave()
1719
+ push('/success') // stable reference
1720
+ }
1721
+
1722
+ return <Button onPress={handlePress}>Save</Button>
1723
+ }
1724
+ ```
1725
+
1726
+ ### 8.2 Use .get() and .set() for Reanimated Shared Values (not .value)
1727
+
1728
+ **Impact: LOW (required for React Compiler compatibility)**
1729
+
1730
+ With React Compiler enabled, use `.get()` and `.set()` instead of reading or
1731
+
1732
+ writing `.value` directly on Reanimated shared values. The compiler can't track
1733
+
1734
+ property access—explicit methods ensure correct behavior.
1735
+
1736
+ **Incorrect: breaks with React Compiler**
1737
+
1738
+ ```tsx
1739
+ import { useSharedValue } from 'react-native-reanimated'
1740
+
1741
+ function Counter() {
1742
+ const count = useSharedValue(0)
1743
+
1744
+ const increment = () => {
1745
+ count.value = count.value + 1 // opts out of react compiler
1746
+ }
1747
+
1748
+ return <Button onPress={increment} title={`Count: ${count.value}`} />
1749
+ }
1750
+ ```
1751
+
1752
+ **Correct: React Compiler compatible**
1753
+
1754
+ ```tsx
1755
+ import { useSharedValue } from 'react-native-reanimated'
1756
+
1757
+ function Counter() {
1758
+ const count = useSharedValue(0)
1759
+
1760
+ const increment = () => {
1761
+ count.set(count.get() + 1)
1762
+ }
1763
+
1764
+ return <Button onPress={increment} title={`Count: ${count.get()}`} />
1765
+ }
1766
+ ```
1767
+
1768
+ See the
1769
+
1770
+ [Reanimated docs](https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support)
1771
+
1772
+ for more.
1773
+
1774
+ ---
1775
+
1776
+ ## 9. User Interface
1777
+
1778
+ **Impact: MEDIUM**
1779
+
1780
+ Native UI patterns for images, menus, modals, styling, and
1781
+ platform-consistent interfaces.
1782
+
1783
+ ### 9.1 Measuring View Dimensions
1784
+
1785
+ **Impact: MEDIUM (synchronous measurement, avoid unnecessary re-renders)**
1786
+
1787
+ Use both `useLayoutEffect` (synchronous) and `onLayout` (for updates). The sync
1788
+
1789
+ measurement gives you the initial size immediately; `onLayout` keeps it current
1790
+
1791
+ when the view changes. For non-primitive states, use a dispatch updater to
1792
+
1793
+ compare values and avoid unnecessary re-renders.
1794
+
1795
+ **Height only:**
1796
+
1797
+ ```tsx
1798
+ import { useLayoutEffect, useRef, useState } from 'react'
1799
+ import { View, LayoutChangeEvent } from 'react-native'
1800
+
1801
+ function MeasuredBox({ children }: { children: React.ReactNode }) {
1802
+ const ref = useRef<View>(null)
1803
+ const [height, setHeight] = useState<number | undefined>(undefined)
1804
+
1805
+ 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
+ }, [])
1811
+
1812
+ const onLayout = (e: LayoutChangeEvent) => {
1813
+ setHeight(e.nativeEvent.layout.height)
1814
+ }
1815
+
1816
+ return (
1817
+ <View ref={ref} onLayout={onLayout}>
1818
+ {children}
1819
+ </View>
1820
+ )
1821
+ }
1822
+ ```
1823
+
1824
+ **Both dimensions:**
1825
+
1826
+ ```tsx
1827
+ import { useLayoutEffect, useRef, useState } from 'react'
1828
+ import { View, LayoutChangeEvent } from 'react-native'
1829
+
1830
+ type Size = { width: number; height: number }
1831
+
1832
+ function MeasuredBox({ children }: { children: React.ReactNode }) {
1833
+ const ref = useRef<View>(null)
1834
+ const [size, setSize] = useState<Size | undefined>(undefined)
1835
+
1836
+ useLayoutEffect(() => {
1837
+ const rect = ref.current?.getBoundingClientRect()
1838
+ if (rect) setSize({ width: rect.width, height: rect.height })
1839
+ }, [])
1840
+
1841
+ const onLayout = (e: LayoutChangeEvent) => {
1842
+ const { width, height } = e.nativeEvent.layout
1843
+ setSize((prev) => {
1844
+ // for non-primitive states, compare values before firing a re-render
1845
+ if (prev?.width === width && prev?.height === height) return prev
1846
+ return { width, height }
1847
+ })
1848
+ }
1849
+
1850
+ return (
1851
+ <View ref={ref} onLayout={onLayout}>
1852
+ {children}
1853
+ </View>
1854
+ )
1855
+ }
1856
+ ```
1857
+
1858
+ Use functional setState to compare—don't read state directly in the callback.
1859
+
1860
+ ### 9.2 Modern React Native Styling Patterns
1861
+
1862
+ **Impact: MEDIUM (consistent design, smoother borders, cleaner layouts)**
1863
+
1864
+ Follow these styling patterns for cleaner, more consistent React Native code.
1865
+
1866
+ **Always use `borderCurve: 'continuous'` with `borderRadius`:**
1867
+
1868
+ **Use `gap` instead of margin for spacing between elements:**
1869
+
1870
+ ```tsx
1871
+ // Incorrect – margin on children
1872
+ <View>
1873
+ <Text style={{ marginBottom: 8 }}>Title</Text>
1874
+ <Text style={{ marginBottom: 8 }}>Subtitle</Text>
1875
+ </View>
1876
+
1877
+ // Correct – gap on parent
1878
+ <View style={{ gap: 8 }}>
1879
+ <Text>Title</Text>
1880
+ <Text>Subtitle</Text>
1881
+ </View>
1882
+ ```
1883
+
1884
+ **Use `padding` for space within, `gap` for space between:**
1885
+
1886
+ ```tsx
1887
+ <View style={{ padding: 16, gap: 12 }}>
1888
+ <Text>First</Text>
1889
+ <Text>Second</Text>
1890
+ </View>
1891
+ ```
1892
+
1893
+ **Use `experimental_backgroundImage` for linear gradients:**
1894
+
1895
+ ```tsx
1896
+ // Incorrect – third-party gradient library
1897
+ <LinearGradient colors={['#000', '#fff']} />
1898
+
1899
+ // Correct – native CSS gradient syntax
1900
+ <View
1901
+ style={{
1902
+ experimental_backgroundImage: 'linear-gradient(to bottom, #000, #fff)',
1903
+ }}
1904
+ />
1905
+ ```
1906
+
1907
+ **Use CSS `boxShadow` string syntax for shadows:**
1908
+
1909
+ ```tsx
1910
+ // Incorrect – legacy shadow objects or elevation
1911
+ { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1 }
1912
+ { elevation: 4 }
1913
+
1914
+ // Correct – CSS box-shadow syntax
1915
+ { boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' }
1916
+ ```
1917
+
1918
+ **Avoid multiple font sizes – use weight and color for emphasis:**
1919
+
1920
+ ```tsx
1921
+ // Incorrect – varying font sizes for hierarchy
1922
+ <Text style={{ fontSize: 18 }}>Title</Text>
1923
+ <Text style={{ fontSize: 14 }}>Subtitle</Text>
1924
+ <Text style={{ fontSize: 12 }}>Caption</Text>
1925
+
1926
+ // Correct – consistent size, vary weight and color
1927
+ <Text style={{ fontWeight: '600' }}>Title</Text>
1928
+ <Text style={{ color: '#666' }}>Subtitle</Text>
1929
+ <Text style={{ color: '#999' }}>Caption</Text>
1930
+ ```
1931
+
1932
+ Limiting font sizes creates visual consistency. Use `fontWeight` (bold/semibold)
1933
+
1934
+ and grayscale colors for hierarchy instead.
1935
+
1936
+ ### 9.3 Use contentInset for Dynamic ScrollView Spacing
1937
+
1938
+ **Impact: LOW (smoother updates, no layout recalculation)**
1939
+
1940
+ When adding space to the top or bottom of a ScrollView that may change
1941
+
1942
+ (keyboard, toolbars, dynamic content), use `contentInset` instead of padding.
1943
+
1944
+ Changing `contentInset` doesn't trigger layout recalculation—it adjusts the
1945
+
1946
+ scroll area without re-rendering content.
1947
+
1948
+ **Incorrect: padding causes layout recalculation**
1949
+
1950
+ ```tsx
1951
+ function Feed({ bottomOffset }: { bottomOffset: number }) {
1952
+ return (
1953
+ <ScrollView contentContainerStyle={{ paddingBottom: bottomOffset }}>
1954
+ {children}
1955
+ </ScrollView>
1956
+ )
1957
+ }
1958
+ // Changing bottomOffset triggers full layout recalculation
1959
+ ```
1960
+
1961
+ **Correct: contentInset for dynamic spacing**
1962
+
1963
+ ```tsx
1964
+ function Feed({ bottomOffset }: { bottomOffset: number }) {
1965
+ return (
1966
+ <ScrollView
1967
+ contentInset={{ bottom: bottomOffset }}
1968
+ scrollIndicatorInsets={{ bottom: bottomOffset }}
1969
+ >
1970
+ {children}
1971
+ </ScrollView>
1972
+ )
1973
+ }
1974
+ // Changing bottomOffset only adjusts scroll bounds
1975
+ ```
1976
+
1977
+ Use `scrollIndicatorInsets` alongside `contentInset` to keep the scroll
1978
+
1979
+ indicator aligned. For static spacing that never changes, padding is fine.
1980
+
1981
+ ### 9.4 Use contentInsetAdjustmentBehavior for Safe Areas
1982
+
1983
+ **Impact: MEDIUM (native safe area handling, no layout shifts)**
1984
+
1985
+ Use `contentInsetAdjustmentBehavior="automatic"` on the root ScrollView instead of wrapping content in SafeAreaView or manual padding. This lets iOS handle safe area insets natively with proper scroll behavior.
1986
+
1987
+ **Incorrect: SafeAreaView wrapper**
1988
+
1989
+ ```tsx
1990
+ import { SafeAreaView, ScrollView, View, Text } from 'react-native'
1991
+
1992
+ function 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
+ ```
2004
+
2005
+ **Incorrect: manual safe area padding**
2006
+
2007
+ ```tsx
2008
+ import { ScrollView, View, Text } from 'react-native'
2009
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
2010
+
2011
+ function MyScreen() {
2012
+ const insets = useSafeAreaInsets()
2013
+
2014
+ return (
2015
+ <ScrollView contentContainerStyle={{ paddingTop: insets.top }}>
2016
+ <View>
2017
+ <Text>Content</Text>
2018
+ </View>
2019
+ </ScrollView>
2020
+ )
2021
+ }
2022
+ ```
2023
+
2024
+ **Correct: native content inset adjustment**
2025
+
2026
+ ```tsx
2027
+ import { ScrollView, View, Text } from 'react-native'
2028
+
2029
+ function MyScreen() {
2030
+ return (
2031
+ <ScrollView contentInsetAdjustmentBehavior='automatic'>
2032
+ <View>
2033
+ <Text>Content</Text>
2034
+ </View>
2035
+ </ScrollView>
2036
+ )
2037
+ }
2038
+ ```
2039
+
2040
+ The native approach handles dynamic safe areas (keyboard, toolbars) and allows content to scroll behind the status bar naturally.
2041
+
2042
+ ### 9.5 Use expo-image for Optimized Images
2043
+
2044
+ **Impact: HIGH (memory efficiency, caching, blurhash placeholders, progressive loading)**
2045
+
2046
+ Use `expo-image` instead of React Native's `Image`. It provides memory-efficient caching, blurhash placeholders, progressive loading, and better performance for lists.
2047
+
2048
+ **Incorrect: React Native Image**
2049
+
2050
+ ```tsx
2051
+ import { Image } from 'react-native'
2052
+
2053
+ function Avatar({ url }: { url: string }) {
2054
+ return <Image source={{ uri: url }} style={styles.avatar} />
2055
+ }
2056
+ ```
2057
+
2058
+ **Correct: expo-image**
2059
+
2060
+ ```tsx
2061
+ import { Image } from 'expo-image'
2062
+
2063
+ function Avatar({ url }: { url: string }) {
2064
+ return <Image source={{ uri: url }} style={styles.avatar} />
2065
+ }
2066
+ ```
2067
+
2068
+ **With blurhash placeholder:**
2069
+
2070
+ ```tsx
2071
+ <Image
2072
+ 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
+ ```
2079
+
2080
+ **With priority and caching:**
2081
+
2082
+ ```tsx
2083
+ <Image
2084
+ source={{ uri: url }}
2085
+ priority="high"
2086
+ cachePolicy="memory-disk"
2087
+ style={styles.hero}
2088
+ />
2089
+ ```
2090
+
2091
+ **Key props:**
2092
+
2093
+ - `placeholder` — Blurhash or thumbnail while loading
2094
+
2095
+ - `contentFit` — `cover`, `contain`, `fill`, `scale-down`
2096
+
2097
+ - `transition` — Fade-in duration (ms)
2098
+
2099
+ - `priority` — `low`, `normal`, `high`
2100
+
2101
+ - `cachePolicy` — `memory`, `disk`, `memory-disk`, `none`
2102
+
2103
+ - `recyclingKey` — Unique key for list recycling
2104
+
2105
+ For cross-platform (web + native), use `SolitoImage` from `solito/image` which uses `expo-image` under the hood.
2106
+
2107
+ Reference: [https://docs.expo.dev/versions/latest/sdk/image/](https://docs.expo.dev/versions/latest/sdk/image/)
2108
+
2109
+ ### 9.6 Use Galeria for Image Galleries and Lightbox
2110
+
2111
+ **Impact: MEDIUM**
2112
+
2113
+ For image galleries with lightbox (tap to fullscreen), use `@nandorojo/galeria`.
2114
+
2115
+ It provides native shared element transitions with pinch-to-zoom, double-tap
2116
+
2117
+ zoom, and pan-to-close. Works with any image component including `expo-image`.
2118
+
2119
+ **Incorrect: custom modal implementation**
2120
+
2121
+ ```tsx
2122
+ function ImageGallery({ urls }: { urls: string[] }) {
2123
+ const [selected, setSelected] = useState<string | null>(null)
2124
+
2125
+ 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
+ ```
2139
+
2140
+ **Correct: Galeria with expo-image**
2141
+
2142
+ ```tsx
2143
+ import { Galeria } from '@nandorojo/galeria'
2144
+ import { Image } from 'expo-image'
2145
+
2146
+ function 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
+ ```
2158
+
2159
+ **Single image:**
2160
+
2161
+ ```tsx
2162
+ import { Galeria } from '@nandorojo/galeria'
2163
+ import { Image } from 'expo-image'
2164
+
2165
+ function 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
+ ```
2175
+
2176
+ **With low-res thumbnails and high-res fullscreen:**
2177
+
2178
+ ```tsx
2179
+ <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
+ ```
2187
+
2188
+ **With FlashList:**
2189
+
2190
+ ```tsx
2191
+ <Galeria urls={urls}>
2192
+ <FlashList
2193
+ 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
+ ```
2204
+
2205
+ Works with `expo-image`, `SolitoImage`, `react-native` Image, or any image
2206
+
2207
+ component.
2208
+
2209
+ Reference: [https://github.com/nandorojo/galeria](https://github.com/nandorojo/galeria)
2210
+
2211
+ ### 9.7 Use Native Menus for Dropdowns and Context Menus
2212
+
2213
+ **Impact: HIGH (native accessibility, platform-consistent UX)**
2214
+
2215
+ Use native platform menus instead of custom JS implementations. Native menus
2216
+
2217
+ provide built-in accessibility, consistent platform UX, and better performance.
2218
+
2219
+ Use [zeego](https://zeego.dev) for cross-platform native menus.
2220
+
2221
+ **Incorrect: custom JS menu**
2222
+
2223
+ ```tsx
2224
+ import { useState } from 'react'
2225
+ import { View, Pressable, Text } from 'react-native'
2226
+
2227
+ function MyMenu() {
2228
+ const [open, setOpen] = useState(false)
2229
+
2230
+ 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
+ ```
2249
+
2250
+ **Correct: native menu with zeego**
2251
+
2252
+ ```tsx
2253
+ import * as DropdownMenu from 'zeego/dropdown-menu'
2254
+
2255
+ function MyMenu() {
2256
+ return (
2257
+ <DropdownMenu.Root>
2258
+ <DropdownMenu.Trigger>
2259
+ <Pressable>
2260
+ <Text>Open Menu</Text>
2261
+ </Pressable>
2262
+ </DropdownMenu.Trigger>
2263
+
2264
+ <DropdownMenu.Content>
2265
+ <DropdownMenu.Item key='edit' onSelect={() => console.log('edit')}>
2266
+ <DropdownMenu.ItemTitle>Edit</DropdownMenu.ItemTitle>
2267
+ </DropdownMenu.Item>
2268
+
2269
+ <DropdownMenu.Item
2270
+ key='delete'
2271
+ destructive
2272
+ onSelect={() => console.log('delete')}
2273
+ >
2274
+ <DropdownMenu.ItemTitle>Delete</DropdownMenu.ItemTitle>
2275
+ </DropdownMenu.Item>
2276
+ </DropdownMenu.Content>
2277
+ </DropdownMenu.Root>
2278
+ )
2279
+ }
2280
+ ```
2281
+
2282
+ **Context menu: long-press**
2283
+
2284
+ ```tsx
2285
+ import * as ContextMenu from 'zeego/context-menu'
2286
+
2287
+ function 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>
2295
+
2296
+ <ContextMenu.Content>
2297
+ <ContextMenu.Item key='copy' onSelect={() => console.log('copy')}>
2298
+ <ContextMenu.ItemTitle>Copy</ContextMenu.ItemTitle>
2299
+ </ContextMenu.Item>
2300
+
2301
+ <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
+ ```
2309
+
2310
+ **Checkbox items:**
2311
+
2312
+ ```tsx
2313
+ import * as DropdownMenu from 'zeego/dropdown-menu'
2314
+
2315
+ function SettingsMenu() {
2316
+ const [notifications, setNotifications] = useState(true)
2317
+
2318
+ return (
2319
+ <DropdownMenu.Root>
2320
+ <DropdownMenu.Trigger>
2321
+ <Pressable>
2322
+ <Text>Settings</Text>
2323
+ </Pressable>
2324
+ </DropdownMenu.Trigger>
2325
+
2326
+ <DropdownMenu.Content>
2327
+ <DropdownMenu.CheckboxItem
2328
+ 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
+ ```
2340
+
2341
+ **Submenus:**
2342
+
2343
+ ```tsx
2344
+ import * as DropdownMenu from 'zeego/dropdown-menu'
2345
+
2346
+ function MenuWithSubmenu() {
2347
+ return (
2348
+ <DropdownMenu.Root>
2349
+ <DropdownMenu.Trigger>
2350
+ <Pressable>
2351
+ <Text>Options</Text>
2352
+ </Pressable>
2353
+ </DropdownMenu.Trigger>
2354
+
2355
+ <DropdownMenu.Content>
2356
+ <DropdownMenu.Item key='home' onSelect={() => console.log('home')}>
2357
+ <DropdownMenu.ItemTitle>Home</DropdownMenu.ItemTitle>
2358
+ </DropdownMenu.Item>
2359
+
2360
+ <DropdownMenu.Sub>
2361
+ <DropdownMenu.SubTrigger key='more'>
2362
+ <DropdownMenu.ItemTitle>More Options</DropdownMenu.ItemTitle>
2363
+ </DropdownMenu.SubTrigger>
2364
+
2365
+ <DropdownMenu.SubContent>
2366
+ <DropdownMenu.Item key='settings'>
2367
+ <DropdownMenu.ItemTitle>Settings</DropdownMenu.ItemTitle>
2368
+ </DropdownMenu.Item>
2369
+
2370
+ <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
+ ```
2380
+
2381
+ Reference: [https://zeego.dev/components/dropdown-menu](https://zeego.dev/components/dropdown-menu)
2382
+
2383
+ ### 9.8 Use Native Modals Over JS-Based Bottom Sheets
2384
+
2385
+ **Impact: HIGH (native performance, gestures, accessibility)**
2386
+
2387
+ Use native `<Modal>` with `presentationStyle="formSheet"` or React Navigation
2388
+
2389
+ v7's native form sheet instead of JS-based bottom sheet libraries. Native modals
2390
+
2391
+ have built-in gestures, accessibility, and better performance. Rely on native UI
2392
+
2393
+ for low-level primitives.
2394
+
2395
+ **Incorrect: JS-based bottom sheet**
2396
+
2397
+ ```tsx
2398
+ import BottomSheet from 'custom-js-bottom-sheet'
2399
+
2400
+ function MyScreen() {
2401
+ const sheetRef = useRef<BottomSheet>(null)
2402
+
2403
+ 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
+ ```
2415
+
2416
+ **Correct: native Modal with formSheet**
2417
+
2418
+ ```tsx
2419
+ import { Modal, View, Text, Button } from 'react-native'
2420
+
2421
+ function MyScreen() {
2422
+ const [visible, setVisible] = useState(false)
2423
+
2424
+ return (
2425
+ <View style={{ flex: 1 }}>
2426
+ <Button onPress={() => setVisible(true)} title='Open' />
2427
+ <Modal
2428
+ 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
+ ```
2441
+
2442
+ **Correct: React Navigation v7 native form sheet**
2443
+
2444
+ ```tsx
2445
+ // In your navigator
2446
+ <Stack.Screen
2447
+ name='Details'
2448
+ component={DetailsScreen}
2449
+ options={{
2450
+ presentation: 'formSheet',
2451
+ sheetAllowedDetents: 'fitToContents',
2452
+ }}
2453
+ />
2454
+ ```
2455
+
2456
+ Native modals provide swipe-to-dismiss, proper keyboard avoidance, and
2457
+
2458
+ accessibility out of the box.
2459
+
2460
+ ### 9.9 Use Pressable Instead of Touchable Components
2461
+
2462
+ **Impact: LOW (modern API, more flexible)**
2463
+
2464
+ Never use `TouchableOpacity` or `TouchableHighlight`. Use `Pressable` from
2465
+
2466
+ `react-native` or `react-native-gesture-handler` instead.
2467
+
2468
+ **Incorrect: legacy Touchable components**
2469
+
2470
+ ```tsx
2471
+ import { TouchableOpacity } from 'react-native'
2472
+
2473
+ function MyButton({ onPress }: { onPress: () => void }) {
2474
+ return (
2475
+ <TouchableOpacity onPress={onPress} activeOpacity={0.7}>
2476
+ <Text>Press me</Text>
2477
+ </TouchableOpacity>
2478
+ )
2479
+ }
2480
+ ```
2481
+
2482
+ **Correct: Pressable**
2483
+
2484
+ ```tsx
2485
+ import { Pressable } from 'react-native'
2486
+
2487
+ function MyButton({ onPress }: { onPress: () => void }) {
2488
+ return (
2489
+ <Pressable onPress={onPress}>
2490
+ <Text>Press me</Text>
2491
+ </Pressable>
2492
+ )
2493
+ }
2494
+ ```
2495
+
2496
+ **Correct: Pressable from gesture handler for lists**
2497
+
2498
+ ```tsx
2499
+ import { Pressable } from 'react-native-gesture-handler'
2500
+
2501
+ function ListItem({ onPress }: { onPress: () => void }) {
2502
+ return (
2503
+ <Pressable onPress={onPress}>
2504
+ <Text>Item</Text>
2505
+ </Pressable>
2506
+ )
2507
+ }
2508
+ ```
2509
+
2510
+ Use `react-native-gesture-handler` Pressable inside scrollable lists for better
2511
+
2512
+ gesture coordination, as long as you are using the ScrollView from
2513
+
2514
+ `react-native-gesture-handler` as well.
2515
+
2516
+ **For animated press states (scale, opacity changes):** Use `GestureDetector`
2517
+
2518
+ with Reanimated shared values instead of Pressable's style callback. See the
2519
+
2520
+ `animation-gesture-detector-press` rule.
2521
+
2522
+ ---
2523
+
2524
+ ## 10. Design System
2525
+
2526
+ **Impact: MEDIUM**
2527
+
2528
+ Architecture patterns for building maintainable component
2529
+ libraries.
2530
+
2531
+ ### 10.1 Use Compound Components Over Polymorphic Children
2532
+
2533
+ **Impact: MEDIUM (flexible composition, clearer API)**
2534
+
2535
+ Don't create components that can accept a string if they aren't a text node. If
2536
+
2537
+ a component can receive a string child, it must be a dedicated `*Text`
2538
+
2539
+ component. For components like buttons, which can have both a View (or
2540
+
2541
+ Pressable) together with text, use compound components, such a `Button`,
2542
+
2543
+ `ButtonText`, and `ButtonIcon`.
2544
+
2545
+ **Incorrect: polymorphic children**
2546
+
2547
+ ```tsx
2548
+ import { Pressable, Text } from 'react-native'
2549
+
2550
+ type ButtonProps = {
2551
+ children: string | React.ReactNode
2552
+ icon?: React.ReactNode
2553
+ }
2554
+
2555
+ function Button({ children, icon }: ButtonProps) {
2556
+ return (
2557
+ <Pressable>
2558
+ {icon}
2559
+ {typeof children === 'string' ? <Text>{children}</Text> : children}
2560
+ </Pressable>
2561
+ )
2562
+ }
2563
+
2564
+ // Usage is ambiguous
2565
+ <Button icon={<Icon />}>Save</Button>
2566
+ <Button><CustomText>Save</CustomText></Button>
2567
+ ```
2568
+
2569
+ **Correct: compound components**
2570
+
2571
+ ```tsx
2572
+ import { Pressable, Text } from 'react-native'
2573
+
2574
+ function Button({ children }: { children: React.ReactNode }) {
2575
+ return <Pressable>{children}</Pressable>
2576
+ }
2577
+
2578
+ function ButtonText({ children }: { children: React.ReactNode }) {
2579
+ return <Text>{children}</Text>
2580
+ }
2581
+
2582
+ function ButtonIcon({ children }: { children: React.ReactNode }) {
2583
+ return <>{children}</>
2584
+ }
2585
+
2586
+ // Usage is explicit and composable
2587
+ <Button>
2588
+ <ButtonIcon><SaveIcon /></ButtonIcon>
2589
+ <ButtonText>Save</ButtonText>
2590
+ </Button>
2591
+
2592
+ <Button>
2593
+ <ButtonText>Cancel</ButtonText>
2594
+ </Button>
2595
+ ```
2596
+
2597
+ ---
2598
+
2599
+ ## 11. Monorepo
2600
+
2601
+ **Impact: LOW**
2602
+
2603
+ Dependency management and native module configuration in
2604
+ monorepos.
2605
+
2606
+ ### 11.1 Install Native Dependencies in App Directory
2607
+
2608
+ **Impact: CRITICAL (required for autolinking to work)**
2609
+
2610
+ In a monorepo, packages with native code must be installed in the native app's
2611
+
2612
+ directory directly. Autolinking only scans the app's `node_modules`—it won't
2613
+
2614
+ find native dependencies installed in other packages.
2615
+
2616
+ **Incorrect: native dep in shared package only**
2617
+
2618
+ ```typescript
2619
+ packages/
2620
+ ui/
2621
+ package.json # has react-native-reanimated
2622
+ app/
2623
+ package.json # missing react-native-reanimated
2624
+ ```
2625
+
2626
+ Autolinking fails—native code not linked.
2627
+
2628
+ **Correct: native dep in app directory**
2629
+
2630
+ ```json
2631
+ // packages/app/package.json
2632
+ {
2633
+ "dependencies": {
2634
+ "react-native-reanimated": "3.16.1"
2635
+ }
2636
+ }
2637
+ ```
2638
+
2639
+ Even if the shared package uses the native dependency, the app must also list it
2640
+
2641
+ for autolinking to detect and link the native code.
2642
+
2643
+ ### 11.2 Use Single Dependency Versions Across Monorepo
2644
+
2645
+ **Impact: MEDIUM (avoids duplicate bundles, version conflicts)**
2646
+
2647
+ Use a single version of each dependency across all packages in your monorepo.
2648
+
2649
+ Prefer exact versions over ranges. Multiple versions cause duplicate code in
2650
+
2651
+ bundles, runtime conflicts, and inconsistent behavior across packages.
2652
+
2653
+ Use a tool like syncpack to enforce this. As a last resort, use yarn resolutions
2654
+
2655
+ or npm overrides.
2656
+
2657
+ **Incorrect: version ranges, multiple versions**
2658
+
2659
+ ```json
2660
+ // packages/app/package.json
2661
+ {
2662
+ "dependencies": {
2663
+ "react-native-reanimated": "^3.0.0"
2664
+ }
2665
+ }
2666
+
2667
+ // packages/ui/package.json
2668
+ {
2669
+ "dependencies": {
2670
+ "react-native-reanimated": "^3.5.0"
2671
+ }
2672
+ }
2673
+ ```
2674
+
2675
+ **Correct: exact versions, single source of truth**
2676
+
2677
+ ```json
2678
+ // package.json (root)
2679
+ {
2680
+ "pnpm": {
2681
+ "overrides": {
2682
+ "react-native-reanimated": "3.16.1"
2683
+ }
2684
+ }
2685
+ }
2686
+
2687
+ // packages/app/package.json
2688
+ {
2689
+ "dependencies": {
2690
+ "react-native-reanimated": "3.16.1"
2691
+ }
2692
+ }
2693
+
2694
+ // packages/ui/package.json
2695
+ {
2696
+ "dependencies": {
2697
+ "react-native-reanimated": "3.16.1"
2698
+ }
2699
+ }
2700
+ ```
2701
+
2702
+ Use your package manager's override/resolution feature to enforce versions at
2703
+
2704
+ the root. When adding dependencies, specify exact versions without `^` or `~`.
2705
+
2706
+ ---
2707
+
2708
+ ## 12. Third-Party Dependencies
2709
+
2710
+ **Impact: LOW**
2711
+
2712
+ Wrapping and re-exporting third-party dependencies for
2713
+ maintainability.
2714
+
2715
+ ### 12.1 Import from Design System Folder
2716
+
2717
+ **Impact: LOW (enables global changes and easy refactoring)**
2718
+
2719
+ Re-export dependencies from a design system folder. App code imports from there,
2720
+
2721
+ not directly from packages. This enables global changes and easy refactoring.
2722
+
2723
+ **Incorrect: imports directly from package**
2724
+
2725
+ ```tsx
2726
+ import { View, Text } from 'react-native'
2727
+ import { Button } from '@ui/button'
2728
+
2729
+ function Profile() {
2730
+ return (
2731
+ <View>
2732
+ <Text>Hello</Text>
2733
+ <Button>Save</Button>
2734
+ </View>
2735
+ )
2736
+ }
2737
+ ```
2738
+
2739
+ **Correct: imports from design system**
2740
+
2741
+ ```tsx
2742
+ import { View } from '@/components/view'
2743
+ import { Text } from '@/components/text'
2744
+ import { Button } from '@/components/button'
2745
+
2746
+ function Profile() {
2747
+ return (
2748
+ <View>
2749
+ <Text>Hello</Text>
2750
+ <Button>Save</Button>
2751
+ </View>
2752
+ )
2753
+ }
2754
+ ```
2755
+
2756
+ Start by simply re-exporting. Customize later without changing app code.
2757
+
2758
+ ---
2759
+
2760
+ ## 13. JavaScript
2761
+
2762
+ **Impact: LOW**
2763
+
2764
+ Micro-optimizations like hoisting expensive object creation.
2765
+
2766
+ ### 13.1 Hoist Intl Formatter Creation
2767
+
2768
+ **Impact: LOW-MEDIUM (avoids expensive object recreation)**
2769
+
2770
+ Don't create `Intl.DateTimeFormat`, `Intl.NumberFormat`, or
2771
+
2772
+ `Intl.RelativeTimeFormat` inside render or loops. These are expensive to
2773
+
2774
+ instantiate. Hoist to module scope when the locale/options are static.
2775
+
2776
+ **Incorrect: new formatter every render**
2777
+
2778
+ ```tsx
2779
+ function 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
+ ```
2787
+
2788
+ **Correct: hoisted to module scope**
2789
+
2790
+ ```tsx
2791
+ const currencyFormatter = new Intl.NumberFormat('en-US', {
2792
+ style: 'currency',
2793
+ currency: 'USD',
2794
+ })
2795
+
2796
+ function Price({ amount }: { amount: number }) {
2797
+ return <Text>{currencyFormatter.format(amount)}</Text>
2798
+ }
2799
+ ```
2800
+
2801
+ **For dynamic locales, memoize:**
2802
+
2803
+ ```tsx
2804
+ const dateFormatter = useMemo(
2805
+ () => new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }),
2806
+ [locale]
2807
+ )
2808
+ ```
2809
+
2810
+ **Common formatters to hoist:**
2811
+
2812
+ ```tsx
2813
+ // Module-level formatters
2814
+ const dateFormatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' })
2815
+ const timeFormatter = new Intl.DateTimeFormat('en-US', { timeStyle: 'short' })
2816
+ const percentFormatter = new Intl.NumberFormat('en-US', { style: 'percent' })
2817
+ const relativeFormatter = new Intl.RelativeTimeFormat('en-US', {
2818
+ numeric: 'auto',
2819
+ })
2820
+ ```
2821
+
2822
+ Creating `Intl` objects is significantly more expensive than `RegExp` or plain
2823
+
2824
+ objects—each instantiation parses locale data and builds internal lookup tables.
2825
+
2826
+ ---
2827
+
2828
+ ## 14. Fonts
2829
+
2830
+ **Impact: LOW**
2831
+
2832
+ Native font loading for improved performance.
2833
+
2834
+ ### 14.1 Load fonts natively at build time
2835
+
2836
+ **Impact: LOW (fonts available at launch, no async loading)**
2837
+
2838
+ Use the `expo-font` config plugin to embed fonts at build time instead of
2839
+
2840
+ `useFonts` or `Font.loadAsync`. Embedded fonts are more efficient.
2841
+
2842
+ [Expo Font Documentation](https://docs.expo.dev/versions/latest/sdk/font/)
2843
+
2844
+ **Incorrect: async font loading**
2845
+
2846
+ ```tsx
2847
+ import { useFonts } from 'expo-font'
2848
+ import { Text, View } from 'react-native'
2849
+
2850
+ function App() {
2851
+ const [fontsLoaded] = useFonts({
2852
+ 'Geist-Bold': require('./assets/fonts/Geist-Bold.otf'),
2853
+ })
2854
+
2855
+ if (!fontsLoaded) {
2856
+ return null
2857
+ }
2858
+
2859
+ return (
2860
+ <View>
2861
+ <Text style={{ fontFamily: 'Geist-Bold' }}>Hello</Text>
2862
+ </View>
2863
+ )
2864
+ }
2865
+ ```
2866
+
2867
+ **Correct: config plugin, fonts embedded at build**
2868
+
2869
+ ```tsx
2870
+ import { Text, View } from 'react-native'
2871
+
2872
+ function App() {
2873
+ // No loading state needed—font is already available
2874
+ return (
2875
+ <View>
2876
+ <Text style={{ fontFamily: 'Geist-Bold' }}>Hello</Text>
2877
+ </View>
2878
+ )
2879
+ }
2880
+ ```
2881
+
2882
+ After adding fonts to the config plugin, run `npx expo prebuild` and rebuild the
2883
+
2884
+ native app.
2885
+
2886
+ ---
2887
+
2888
+ ## References
2889
+
2890
+ 1. [https://react.dev](https://react.dev)
2891
+ 2. [https://reactnative.dev](https://reactnative.dev)
2892
+ 3. [https://docs.swmansion.com/react-native-reanimated](https://docs.swmansion.com/react-native-reanimated)
2893
+ 4. [https://docs.swmansion.com/react-native-gesture-handler](https://docs.swmansion.com/react-native-gesture-handler)
2894
+ 5. [https://docs.expo.dev](https://docs.expo.dev)
2895
+ 6. [https://legendapp.com/open-source/legend-list](https://legendapp.com/open-source/legend-list)
2896
+ 7. [https://github.com/nandorojo/galeria](https://github.com/nandorojo/galeria)
2897
+ 8. [https://zeego.dev](https://zeego.dev)