feat: Complete fleet — 94 skills across 10+ domains

Pulled ALL skills from 15 source repositories:
- anthropics/skills: 16 (docs, design, MCP, testing)
- obra/superpowers: 14 (TDD, debugging, agents, planning)
- coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth)
- better-auth/skills: 5 (auth patterns)
- vercel-labs/agent-skills: 5 (React, design, Vercel)
- antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo)
- Plus 13 individual skills from various repos

Mosaic Stack is not limited to coding — the Orchestrator and
subagents serve coding, business, design, marketing, writing,
logistics, analysis, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-16 16:27:42 -06:00
parent 861b28b965
commit f5792c40be
1262 changed files with 212048 additions and 61 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
# React Native Guidelines
A structured repository for creating and maintaining React Native Best Practices
optimized for agents and LLMs.
## Structure
- `rules/` - Individual rule files (one per rule)
- `_sections.md` - Section metadata (titles, impacts, descriptions)
- `_template.md` - Template for creating new rules
- `area-description.md` - Individual rule files
- `metadata.json` - Document metadata (version, organization, abstract)
- **`AGENTS.md`** - Compiled output (generated)
## Rules
### Core Rendering (CRITICAL)
- `rendering-text-in-text-component.md` - Wrap strings in Text components
- `rendering-no-falsy-and.md` - Avoid falsy && operator in JSX
### List Performance (HIGH)
- `list-performance-virtualize.md` - Use virtualized lists (LegendList,
FlashList)
- `list-performance-function-references.md` - Keep stable object references
- `list-performance-callbacks.md` - Hoist callbacks to list root
- `list-performance-inline-objects.md` - Avoid inline objects in renderItem
- `list-performance-item-memo.md` - Pass primitives for memoization
- `list-performance-item-expensive.md` - Keep list items lightweight
- `list-performance-images.md` - Use compressed images in lists
- `list-performance-item-types.md` - Use item types for heterogeneous lists
### Animation (HIGH)
- `animation-gpu-properties.md` - Animate transform/opacity instead of layout
- `animation-gesture-detector-press.md` - Use GestureDetector for press
animations
- `animation-derived-value.md` - Prefer useDerivedValue over useAnimatedReaction
### Scroll Performance (HIGH)
- `scroll-position-no-state.md` - Never track scroll in useState
### Navigation (HIGH)
- `navigation-native-navigators.md` - Use native stack and native tabs
### React State (MEDIUM)
- `react-state-dispatcher.md` - Use functional setState updates
- `react-state-fallback.md` - State should represent user intent only
- `react-state-minimize.md` - Minimize state variables, derive values
### State Architecture (MEDIUM)
- `state-ground-truth.md` - State must represent ground truth
### React Compiler (MEDIUM)
- `react-compiler-destructure-functions.md` - Destructure functions early
- `react-compiler-reanimated-shared-values.md` - Use .get()/.set() for shared
values
### User Interface (MEDIUM)
- `ui-expo-image.md` - Use expo-image for optimized images
- `ui-image-gallery.md` - Use Galeria for lightbox/galleries
- `ui-menus.md` - Native dropdown and context menus with Zeego
- `ui-native-modals.md` - Use native Modal with formSheet
- `ui-pressable.md` - Use Pressable instead of TouchableOpacity
- `ui-measure-views.md` - Measuring view dimensions
- `ui-safe-area-scroll.md` - Use contentInsetAdjustmentBehavior
- `ui-scrollview-content-inset.md` - Use contentInset for dynamic spacing
- `ui-styling.md` - Modern styling patterns (gap, boxShadow, gradients)
### Design System (MEDIUM)
- `design-system-compound-components.md` - Use compound components
### Monorepo (LOW)
- `monorepo-native-deps-in-app.md` - Install native deps in app directory
- `monorepo-single-dependency-versions.md` - Single dependency versions
### Third-Party Dependencies (LOW)
- `imports-design-system-folder.md` - Import from design system folder
### JavaScript (LOW)
- `js-hoist-intl.md` - Hoist Intl formatter creation
### Fonts (LOW)
- `fonts-config-plugin.md` - Load fonts natively at build time
## Creating a New Rule
1. Copy `rules/_template.md` to `rules/area-description.md`
2. Choose the appropriate area prefix:
- `rendering-` for Core Rendering
- `list-performance-` for List Performance
- `animation-` for Animation
- `scroll-` for Scroll Performance
- `navigation-` for Navigation
- `react-state-` for React State
- `state-` for State Architecture
- `react-compiler-` for React Compiler
- `ui-` for User Interface
- `design-system-` for Design System
- `monorepo-` for Monorepo
- `imports-` for Third-Party Dependencies
- `js-` for JavaScript
- `fonts-` for Fonts
3. Fill in the frontmatter and content
4. Ensure you have clear examples with explanations
## Rule File Structure
Each rule file should follow this structure:
````markdown
---
title: Rule Title Here
impact: MEDIUM
impactDescription: Optional description
tags: tag1, tag2, tag3
---
## Rule Title Here
Brief explanation of the rule and why it matters.
**Incorrect (description of what's wrong):**
```tsx
// Bad code example
```
````
**Correct (description of what's right):**
```tsx
// Good code example
```
Reference: [Link](https://example.com)
```
## File Naming Convention
- Files starting with `_` are special (excluded from build)
- Rule files: `area-description.md` (e.g., `animation-gpu-properties.md`)
- Section is automatically inferred from filename prefix
- Rules are sorted alphabetically by title within each section
## Impact Levels
- `CRITICAL` - Highest priority, causes crashes or broken UI
- `HIGH` - Significant performance improvements
- `MEDIUM` - Moderate performance improvements
- `LOW` - Incremental improvements
```

View File

@@ -0,0 +1,121 @@
---
name: vercel-react-native-skills
description:
React Native and Expo best practices for building performant mobile apps. Use
when building React Native components, optimizing list performance,
implementing animations, or working with native modules. Triggers on tasks
involving React Native, Expo, mobile performance, or native platform APIs.
license: MIT
metadata:
author: vercel
version: '1.0.0'
---
# React Native Skills
Comprehensive best practices for React Native and Expo applications. Contains
rules across multiple categories covering performance, animations, UI patterns,
and platform-specific optimizations.
## When to Apply
Reference these guidelines when:
- Building React Native or Expo apps
- Optimizing list and scroll performance
- Implementing animations with Reanimated
- Working with images and media
- Configuring native modules or fonts
- Structuring monorepo projects with native dependencies
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
| -------- | ---------------- | -------- | -------------------- |
| 1 | List Performance | CRITICAL | `list-performance-` |
| 2 | Animation | HIGH | `animation-` |
| 3 | Navigation | HIGH | `navigation-` |
| 4 | UI Patterns | HIGH | `ui-` |
| 5 | State Management | MEDIUM | `react-state-` |
| 6 | Rendering | MEDIUM | `rendering-` |
| 7 | Monorepo | MEDIUM | `monorepo-` |
| 8 | Configuration | LOW | `fonts-`, `imports-` |
## Quick Reference
### 1. List Performance (CRITICAL)
- `list-performance-virtualize` - Use FlashList for large lists
- `list-performance-item-memo` - Memoize list item components
- `list-performance-callbacks` - Stabilize callback references
- `list-performance-inline-objects` - Avoid inline style objects
- `list-performance-function-references` - Extract functions outside render
- `list-performance-images` - Optimize images in lists
- `list-performance-item-expensive` - Move expensive work outside items
- `list-performance-item-types` - Use item types for heterogeneous lists
### 2. Animation (HIGH)
- `animation-gpu-properties` - Animate only transform and opacity
- `animation-derived-value` - Use useDerivedValue for computed animations
- `animation-gesture-detector-press` - Use Gesture.Tap instead of Pressable
### 3. Navigation (HIGH)
- `navigation-native-navigators` - Use native stack and native tabs over JS navigators
### 4. UI Patterns (HIGH)
- `ui-expo-image` - Use expo-image for all images
- `ui-image-gallery` - Use Galeria for image lightboxes
- `ui-pressable` - Use Pressable over TouchableOpacity
- `ui-safe-area-scroll` - Handle safe areas in ScrollViews
- `ui-scrollview-content-inset` - Use contentInset for headers
- `ui-menus` - Use native context menus
- `ui-native-modals` - Use native modals when possible
- `ui-measure-views` - Use onLayout, not measure()
- `ui-styling` - Use StyleSheet.create or Nativewind
### 5. State Management (MEDIUM)
- `react-state-minimize` - Minimize state subscriptions
- `react-state-dispatcher` - Use dispatcher pattern for callbacks
- `react-state-fallback` - Show fallback on first render
- `react-compiler-destructure-functions` - Destructure for React Compiler
- `react-compiler-reanimated-shared-values` - Handle shared values with compiler
### 6. Rendering (MEDIUM)
- `rendering-text-in-text-component` - Wrap text in Text components
- `rendering-no-falsy-and` - Avoid falsy && for conditional rendering
### 7. Monorepo (MEDIUM)
- `monorepo-native-deps-in-app` - Keep native dependencies in app package
- `monorepo-single-dependency-versions` - Use single versions across packages
### 8. Configuration (LOW)
- `fonts-config-plugin` - Use config plugins for custom fonts
- `imports-design-system-folder` - Organize design system imports
- `js-hoist-intl` - Hoist Intl object creation
## How to Use
Read individual rule files for detailed explanations and code examples:
```
rules/list-performance-virtualize.md
rules/animation-gpu-properties.md
```
Each rule file contains:
- Brief explanation of why it matters
- Incorrect code example with explanation
- Correct code example with explanation
- Additional context and references
## Full Compiled Document
For the complete guide with all rules expanded: `AGENTS.md`

View File

@@ -0,0 +1,16 @@
{
"version": "1.0.0",
"organization": "Engineering",
"date": "January 2026",
"abstract": "Comprehensive performance optimization guide for React Native applications, designed for AI agents and LLMs. Contains 35+ rules across 13 categories, prioritized by impact from critical (core rendering, list performance) to incremental (fonts, imports). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.",
"references": [
"https://react.dev",
"https://reactnative.dev",
"https://docs.swmansion.com/react-native-reanimated",
"https://docs.swmansion.com/react-native-gesture-handler",
"https://docs.expo.dev",
"https://legendapp.com/open-source/legend-list",
"https://github.com/nandorojo/galeria",
"https://zeego.dev"
]
}

View File

@@ -0,0 +1,86 @@
# Sections
This file defines all sections, their ordering, impact levels, and descriptions.
The section ID (in parentheses) is the filename prefix used to group rules.
---
## 1. Core Rendering (rendering)
**Impact:** CRITICAL
**Description:** Fundamental React Native rendering rules. Violations cause
runtime crashes or broken UI.
## 2. List Performance (list-performance)
**Impact:** HIGH
**Description:** Optimizing virtualized lists (FlatList, LegendList, FlashList)
for smooth scrolling and fast updates.
## 3. Animation (animation)
**Impact:** HIGH
**Description:** GPU-accelerated animations, Reanimated patterns, and avoiding
render thrashing during gestures.
## 4. Scroll Performance (scroll)
**Impact:** HIGH
**Description:** Tracking scroll position without causing render thrashing.
## 5. Navigation (navigation)
**Impact:** HIGH
**Description:** Using native navigators for stack and tab navigation instead of
JS-based alternatives.
## 6. React State (react-state)
**Impact:** MEDIUM
**Description:** Patterns for managing React state to avoid stale closures and
unnecessary re-renders.
## 7. State Architecture (state)
**Impact:** MEDIUM
**Description:** Ground truth principles for state variables and derived values.
## 8. React Compiler (react-compiler)
**Impact:** MEDIUM
**Description:** Compatibility patterns for React Compiler with React Native and
Reanimated.
## 9. User Interface (ui)
**Impact:** MEDIUM
**Description:** Native UI patterns for images, menus, modals, styling, and
platform-consistent interfaces.
## 10. Design System (design-system)
**Impact:** MEDIUM
**Description:** Architecture patterns for building maintainable component
libraries.
## 11. Monorepo (monorepo)
**Impact:** LOW
**Description:** Dependency management and native module configuration in
monorepos.
## 12. Third-Party Dependencies (imports)
**Impact:** LOW
**Description:** Wrapping and re-exporting third-party dependencies for
maintainability.
## 13. JavaScript (js)
**Impact:** LOW
**Description:** Micro-optimizations like hoisting expensive object creation.
## 14. Fonts (fonts)
**Impact:** LOW
**Description:** Native font loading for improved performance.

View File

@@ -0,0 +1,28 @@
---
title: Rule Title Here
impact: MEDIUM
impactDescription: Optional description of impact (e.g., "20-50% improvement")
tags: tag1, tag2
---
## Rule Title Here
**Impact: MEDIUM (optional impact description)**
Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.
**Incorrect (description of what's wrong):**
```typescript
// Bad code example here
const bad = example()
```
**Correct (description of what's right):**
```typescript
// Good code example here
const good = example()
```
Reference: [Link to documentation or resource](https://example.com)

View File

@@ -0,0 +1,53 @@
---
title: Prefer useDerivedValue Over useAnimatedReaction
impact: MEDIUM
impactDescription: cleaner code, automatic dependency tracking
tags: animation, reanimated, derived-value
---
## Prefer useDerivedValue Over useAnimatedReaction
When deriving a shared value from another, use `useDerivedValue` instead of
`useAnimatedReaction`. Derived values are declarative, automatically track
dependencies, and return a value you can use directly. Animated reactions are
for side effects, not derivations.
**Incorrect (useAnimatedReaction for derivation):**
```tsx
import { useSharedValue, useAnimatedReaction } from 'react-native-reanimated'
function MyComponent() {
const progress = useSharedValue(0)
const opacity = useSharedValue(1)
useAnimatedReaction(
() => progress.value,
(current) => {
opacity.value = 1 - current
}
)
// ...
}
```
**Correct (useDerivedValue):**
```tsx
import { useSharedValue, useDerivedValue } from 'react-native-reanimated'
function MyComponent() {
const progress = useSharedValue(0)
const opacity = useDerivedValue(() => 1 - progress.get())
// ...
}
```
Use `useAnimatedReaction` only for side effects that don't produce a value
(e.g., triggering haptics, logging, calling `runOnJS`).
Reference:
[Reanimated useDerivedValue](https://docs.swmansion.com/react-native-reanimated/docs/core/useDerivedValue)

View File

@@ -0,0 +1,95 @@
---
title: Use GestureDetector for Animated Press States
impact: MEDIUM
impactDescription: UI thread animations, smoother press feedback
tags: animation, gestures, press, reanimated
---
## Use GestureDetector for Animated Press States
For animated press states (scale, opacity on press), use `GestureDetector` with
`Gesture.Tap()` and shared values instead of Pressable's
`onPressIn`/`onPressOut`. Gesture callbacks run on the UI thread as worklets—no
JS thread round-trip for press animations.
**Incorrect (Pressable with JS thread callbacks):**
```tsx
import { Pressable } from 'react-native'
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
} from 'react-native-reanimated'
function AnimatedButton({ onPress }: { onPress: () => void }) {
const scale = useSharedValue(1)
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}))
return (
<Pressable
onPress={onPress}
onPressIn={() => (scale.value = withTiming(0.95))}
onPressOut={() => (scale.value = withTiming(1))}
>
<Animated.View style={animatedStyle}>
<Text>Press me</Text>
</Animated.View>
</Pressable>
)
}
```
**Correct (GestureDetector with UI thread worklets):**
```tsx
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
interpolate,
runOnJS,
} from 'react-native-reanimated'
function AnimatedButton({ onPress }: { onPress: () => void }) {
// Store the press STATE (0 = not pressed, 1 = pressed)
const pressed = useSharedValue(0)
const tap = Gesture.Tap()
.onBegin(() => {
pressed.set(withTiming(1))
})
.onFinalize(() => {
pressed.set(withTiming(0))
})
.onEnd(() => {
runOnJS(onPress)()
})
// Derive visual values from the state
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ scale: interpolate(withTiming(pressed.get()), [0, 1], [1, 0.95]) },
],
}))
return (
<GestureDetector gesture={tap}>
<Animated.View style={animatedStyle}>
<Text>Press me</Text>
</Animated.View>
</GestureDetector>
)
}
```
Store the press **state** (0 or 1), then derive the scale via `interpolate`.
This keeps the shared value as ground truth. Use `runOnJS` to call JS functions
from worklets. Use `.set()` and `.get()` for React Compiler compatibility.
Reference:
[Gesture Handler Tap Gesture](https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture)

View File

@@ -0,0 +1,65 @@
---
title: Animate Transform and Opacity Instead of Layout Properties
impact: HIGH
impactDescription: GPU-accelerated animations, no layout recalculation
tags: animation, performance, reanimated, transform, opacity
---
## Animate Transform and Opacity Instead of Layout Properties
Avoid animating `width`, `height`, `top`, `left`, `margin`, or `padding`. These trigger layout recalculation on every frame. Instead, use `transform` (scale, translate) and `opacity` which run on the GPU without triggering layout.
**Incorrect (animates height, triggers layout every frame):**
```tsx
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
function CollapsiblePanel({ expanded }: { expanded: boolean }) {
const animatedStyle = useAnimatedStyle(() => ({
height: withTiming(expanded ? 200 : 0), // triggers layout on every frame
overflow: 'hidden',
}))
return <Animated.View style={animatedStyle}>{children}</Animated.View>
}
```
**Correct (animates scaleY, GPU-accelerated):**
```tsx
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
function CollapsiblePanel({ expanded }: { expanded: boolean }) {
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ scaleY: withTiming(expanded ? 1 : 0) },
],
opacity: withTiming(expanded ? 1 : 0),
}))
return (
<Animated.View style={[{ height: 200, transformOrigin: 'top' }, animatedStyle]}>
{children}
</Animated.View>
)
}
```
**Correct (animates translateY for slide animations):**
```tsx
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
function SlideIn({ visible }: { visible: boolean }) {
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateY: withTiming(visible ? 0 : 100) },
],
opacity: withTiming(visible ? 1 : 0),
}))
return <Animated.View style={animatedStyle}>{children}</Animated.View>
}
```
GPU-accelerated properties: `transform` (translate, scale, rotate), `opacity`. Everything else triggers layout.

View File

@@ -0,0 +1,66 @@
---
title: Use Compound Components Over Polymorphic Children
impact: MEDIUM
impactDescription: flexible composition, clearer API
tags: design-system, components, composition
---
## Use Compound Components Over Polymorphic Children
Don't create components that can accept a string if they aren't a text node. If
a component can receive a string child, it must be a dedicated `*Text`
component. For components like buttons, which can have both a View (or
Pressable) together with text, use compound components, such a `Button`,
`ButtonText`, and `ButtonIcon`.
**Incorrect (polymorphic children):**
```tsx
import { Pressable, Text } from 'react-native'
type ButtonProps = {
children: string | React.ReactNode
icon?: React.ReactNode
}
function Button({ children, icon }: ButtonProps) {
return (
<Pressable>
{icon}
{typeof children === 'string' ? <Text>{children}</Text> : children}
</Pressable>
)
}
// Usage is ambiguous
<Button icon={<Icon />}>Save</Button>
<Button><CustomText>Save</CustomText></Button>
```
**Correct (compound components):**
```tsx
import { Pressable, Text } from 'react-native'
function Button({ children }: { children: React.ReactNode }) {
return <Pressable>{children}</Pressable>
}
function ButtonText({ children }: { children: React.ReactNode }) {
return <Text>{children}</Text>
}
function ButtonIcon({ children }: { children: React.ReactNode }) {
return <>{children}</>
}
// Usage is explicit and composable
<Button>
<ButtonIcon><SaveIcon /></ButtonIcon>
<ButtonText>Save</ButtonText>
</Button>
<Button>
<ButtonText>Cancel</ButtonText>
</Button>
```

View File

@@ -0,0 +1,71 @@
---
title: Load fonts natively at build time
impact: LOW
impactDescription: fonts available at launch, no async loading
tags: fonts, expo, performance, config-plugin
---
## Use Expo Config Plugin for Font Loading
Use the `expo-font` config plugin to embed fonts at build time instead of
`useFonts` or `Font.loadAsync`. Embedded fonts are more efficient.
**Incorrect (async font loading):**
```tsx
import { useFonts } from 'expo-font'
import { Text, View } from 'react-native'
function App() {
const [fontsLoaded] = useFonts({
'Geist-Bold': require('./assets/fonts/Geist-Bold.otf'),
})
if (!fontsLoaded) {
return null
}
return (
<View>
<Text style={{ fontFamily: 'Geist-Bold' }}>Hello</Text>
</View>
)
}
```
**Correct (config plugin, fonts embedded at build):**
```json
// app.json
{
"expo": {
"plugins": [
[
"expo-font",
{
"fonts": ["./assets/fonts/Geist-Bold.otf"]
}
]
]
}
}
```
```tsx
import { Text, View } from 'react-native'
function App() {
// No loading state needed—font is already available
return (
<View>
<Text style={{ fontFamily: 'Geist-Bold' }}>Hello</Text>
</View>
)
}
```
After adding fonts to the config plugin, run `npx expo prebuild` and rebuild the
native app.
Reference:
[Expo Font Documentation](https://docs.expo.dev/versions/latest/sdk/font/)

View File

@@ -0,0 +1,68 @@
---
title: Import from Design System Folder
impact: LOW
impactDescription: enables global changes and easy refactoring
tags: imports, architecture, design-system
---
## Import from Design System Folder
Re-export dependencies from a design system folder. App code imports from there,
not directly from packages. This enables global changes and easy refactoring.
**Incorrect (imports directly from package):**
```tsx
import { View, Text } from 'react-native'
import { Button } from '@ui/button'
function Profile() {
return (
<View>
<Text>Hello</Text>
<Button>Save</Button>
</View>
)
}
```
**Correct (imports from design system):**
```tsx
// components/view.tsx
import { View as RNView } from 'react-native'
// ideal: pick the props you will actually use to control implementation
export function View(
props: Pick<React.ComponentProps<typeof RNView>, 'style' | 'children'>
) {
return <RNView {...props} />
}
```
```tsx
// components/text.tsx
export { Text } from 'react-native'
```
```tsx
// components/button.tsx
export { Button } from '@ui/button'
```
```tsx
import { View } from '@/components/view'
import { Text } from '@/components/text'
import { Button } from '@/components/button'
function Profile() {
return (
<View>
<Text>Hello</Text>
<Button>Save</Button>
</View>
)
}
```
Start by simply re-exporting. Customize later without changing app code.

View File

@@ -0,0 +1,61 @@
---
title: Hoist Intl Formatter Creation
impact: LOW-MEDIUM
impactDescription: avoids expensive object recreation
tags: javascript, intl, optimization, memoization
---
## Hoist Intl Formatter Creation
Don't create `Intl.DateTimeFormat`, `Intl.NumberFormat`, or
`Intl.RelativeTimeFormat` inside render or loops. These are expensive to
instantiate. Hoist to module scope when the locale/options are static.
**Incorrect (new formatter every render):**
```tsx
function Price({ amount }: { amount: number }) {
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
})
return <Text>{formatter.format(amount)}</Text>
}
```
**Correct (hoisted to module scope):**
```tsx
const currencyFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
})
function Price({ amount }: { amount: number }) {
return <Text>{currencyFormatter.format(amount)}</Text>
}
```
**For dynamic locales, memoize:**
```tsx
const dateFormatter = useMemo(
() => new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }),
[locale]
)
```
**Common formatters to hoist:**
```tsx
// Module-level formatters
const dateFormatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' })
const timeFormatter = new Intl.DateTimeFormat('en-US', { timeStyle: 'short' })
const percentFormatter = new Intl.NumberFormat('en-US', { style: 'percent' })
const relativeFormatter = new Intl.RelativeTimeFormat('en-US', {
numeric: 'auto',
})
```
Creating `Intl` objects is significantly more expensive than `RegExp` or plain
objects—each instantiation parses locale data and builds internal lookup tables.

View File

@@ -0,0 +1,44 @@
---
title: Hoist callbacks to the root of lists
impact: MEDIUM
impactDescription: Fewer re-renders and faster lists
tags: tag1, tag2
---
## List performance callbacks
**Impact: HIGH (Fewer re-renders and faster lists)**
When passing callback functions to list items, create a single instance of the
callback at the root of the list. Items should then call it with a unique
identifier.
**Incorrect (creates a new callback on each render):**
```typescript
return (
<LegendList
renderItem={({ item }) => {
// bad: creates a new callback on each render
const onPress = () => handlePress(item.id)
return <Item key={item.id} item={item} onPress={onPress} />
}}
/>
)
```
**Correct (a single function instance passed to each item):**
```typescript
const onPress = useCallback(() => handlePress(item.id), [handlePress, item.id])
return (
<LegendList
renderItem={({ item }) => (
<Item key={item.id} item={item} onPress={onPress} />
)}
/>
)
```
Reference: [Link to documentation or resource](https://example.com)

View File

@@ -0,0 +1,132 @@
---
title: Optimize List Performance with Stable Object References
impact: CRITICAL
impactDescription: virtualization relies on reference stability
tags: lists, performance, flatlist, virtualization
---
## Optimize List Performance with Stable Object References
Don't map or filter data before passing to virtualized lists. Virtualization
relies on object reference stability to know what changed—new references cause
full re-renders of all visible items. Attempt to prevent frequent renders at the
list-parent level.
Where needed, use context selectors within list items.
**Incorrect (creates new object references on every keystroke):**
```tsx
function DomainSearch() {
const { keyword, setKeyword } = useKeywordZustandState()
const { data: tlds } = useTlds()
// Bad: creates new objects on every render, reparenting the entire list on every keystroke
const domains = tlds.map((tld) => ({
domain: `${keyword}.${tld.name}`,
tld: tld.name,
price: tld.price,
}))
return (
<>
<TextInput value={keyword} onChangeText={setKeyword} />
<LegendList
data={domains}
renderItem={({ item }) => <DomainItem item={item} keyword={keyword} />}
/>
</>
)
}
```
**Correct (stable references, transform inside items):**
```tsx
const renderItem = ({ item }) => <DomainItem tld={item} />
function DomainSearch() {
const { data: tlds } = useTlds()
return (
<LegendList
// good: as long as the data is stable, LegendList will not re-render the entire list
data={tlds}
renderItem={renderItem}
/>
)
}
function DomainItem({ tld }: { tld: Tld }) {
// good: transform within items, and don't pass the dynamic data as a prop
// good: use a selector function from zustand to receive a stable string back
const domain = useKeywordZustandState((s) => s.keyword + '.' + tld.name)
return <Text>{domain}</Text>
}
```
**Updating parent array reference:**
Creating a new array instance can be okay, as long as its inner object
references are stable. For instance, if you sort a list of objects:
```tsx
// good: creates a new array instance without mutating the inner objects
// good: parent array reference is unaffected by typing and updating "keyword"
const sortedTlds = tlds.toSorted((a, b) => a.name.localeCompare(b.name))
return <LegendList data={sortedTlds} renderItem={renderItem} />
```
Even though this creates a new array instance `sortedTlds`, the inner object
references are stable.
**With zustand for dynamic data (avoids parent re-renders):**
```tsx
const useSearchStore = create<{ keyword: string }>(() => ({ keyword: '' }))
function DomainSearch() {
const { data: tlds } = useTlds()
return (
<>
<SearchInput />
<LegendList
data={tlds}
// if you aren't using React Compiler, wrap renderItem with useCallback
renderItem={({ item }) => <DomainItem tld={item} />}
/>
</>
)
}
function DomainItem({ tld }: { tld: Tld }) {
// Select only what you need—component only re-renders when keyword changes
const keyword = useSearchStore((s) => s.keyword)
const domain = `${keyword}.${tld.name}`
return <Text>{domain}</Text>
}
```
Virtualization can now skip items that haven't changed when typing. Only visible
items (~20) re-render on keystroke, rather than the parent.
**Deriving state within list items based on parent data (avoids parent
re-renders):**
For components where the data is conditional based on the parent state, this
pattern is even more important. For example, if you are checking if an item is
favorited, toggling favorites only re-renders one component if the item itself
is in charge of accessing the state rather than the parent:
```tsx
function DomainItemFavoriteButton({ tld }: { tld: Tld }) {
const isFavorited = useFavoritesStore((s) => s.favorites.has(tld.id))
return <TldFavoriteButton isFavorited={isFavorited} />
}
```
Note: if you're using the React Compiler, you can read React Context values
directly within list items. Although this is slightly slower than using a
Zustand selector in most cases, the effect may be negligible.

View File

@@ -0,0 +1,53 @@
---
title: Use Compressed Images in Lists
impact: HIGH
impactDescription: faster load times, less memory
tags: lists, images, performance, optimization
---
## Use Compressed Images in Lists
Always load compressed, appropriately-sized images in lists. Full-resolution
images consume excessive memory and cause scroll jank. Request thumbnails from
your server or use an image CDN with resize parameters.
**Incorrect (full-resolution images):**
```tsx
function ProductItem({ product }: { product: Product }) {
return (
<View>
{/* 4000x3000 image loaded for a 100x100 thumbnail */}
<Image
source={{ uri: product.imageUrl }}
style={{ width: 100, height: 100 }}
/>
<Text>{product.name}</Text>
</View>
)
}
```
**Correct (request appropriately-sized image):**
```tsx
function ProductItem({ product }: { product: Product }) {
// Request a 200x200 image (2x for retina)
const thumbnailUrl = `${product.imageUrl}?w=200&h=200&fit=cover`
return (
<View>
<Image
source={{ uri: thumbnailUrl }}
style={{ width: 100, height: 100 }}
contentFit='cover'
/>
<Text>{product.name}</Text>
</View>
)
}
```
Use an optimized image component with built-in caching and placeholder support,
such as `expo-image` or `SolitoImage` (which uses `expo-image` under the hood).
Request images at 2x the display size for retina screens.

View File

@@ -0,0 +1,97 @@
---
title: Avoid Inline Objects in renderItem
impact: HIGH
impactDescription: prevents unnecessary re-renders of memoized list items
tags: lists, performance, flatlist, virtualization, memo
---
## Avoid Inline Objects in renderItem
Don't create new objects inside `renderItem` to pass as props. Inline objects
create new references on every render, breaking memoization. Pass primitive
values directly from `item` instead.
**Incorrect (inline object breaks memoization):**
```tsx
function UserList({ users }: { users: User[] }) {
return (
<LegendList
data={users}
renderItem={({ item }) => (
<UserRow
// Bad: new object on every render
user={{ id: item.id, name: item.name, avatar: item.avatar }}
/>
)}
/>
)
}
```
**Incorrect (inline style object):**
```tsx
renderItem={({ item }) => (
<UserRow
name={item.name}
// Bad: new style object on every render
style={{ backgroundColor: item.isActive ? 'green' : 'gray' }}
/>
)}
```
**Correct (pass item directly or primitives):**
```tsx
function UserList({ users }: { users: User[] }) {
return (
<LegendList
data={users}
renderItem={({ item }) => (
// Good: pass the item directly
<UserRow user={item} />
)}
/>
)
}
```
**Correct (pass primitives, derive inside child):**
```tsx
renderItem={({ item }) => (
<UserRow
id={item.id}
name={item.name}
isActive={item.isActive}
/>
)}
const UserRow = memo(function UserRow({ id, name, isActive }: Props) {
// Good: derive style inside memoized component
const backgroundColor = isActive ? 'green' : 'gray'
return <View style={[styles.row, { backgroundColor }]}>{/* ... */}</View>
})
```
**Correct (hoist static styles in module scope):**
```tsx
const activeStyle = { backgroundColor: 'green' }
const inactiveStyle = { backgroundColor: 'gray' }
renderItem={({ item }) => (
<UserRow
name={item.name}
// Good: stable references
style={item.isActive ? activeStyle : inactiveStyle}
/>
)}
```
Passing primitives or stable references allows `memo()` to skip re-renders when
the actual values haven't changed.
**Note:** If you have the React Compiler enabled, it handles memoization
automatically and these manual optimizations become less critical.

View File

@@ -0,0 +1,94 @@
---
title: Keep List Items Lightweight
impact: HIGH
impactDescription: reduces render time for visible items during scroll
tags: lists, performance, virtualization, hooks
---
## Keep List Items Lightweight
List items should be as inexpensive as possible to render. Minimize hooks, avoid
queries, and limit React Context access. Virtualized lists render many items
during scroll—expensive items cause jank.
**Incorrect (heavy list item):**
```tsx
function ProductRow({ id }: { id: string }) {
// Bad: query inside list item
const { data: product } = useQuery(['product', id], () => fetchProduct(id))
// Bad: multiple context accesses
const theme = useContext(ThemeContext)
const user = useContext(UserContext)
const cart = useContext(CartContext)
// Bad: expensive computation
const recommendations = useMemo(
() => computeRecommendations(product),
[product]
)
return <View>{/* ... */}</View>
}
```
**Correct (lightweight list item):**
```tsx
function ProductRow({ name, price, imageUrl }: Props) {
// Good: receives only primitives, minimal hooks
return (
<View>
<Image source={{ uri: imageUrl }} />
<Text>{name}</Text>
<Text>{price}</Text>
</View>
)
}
```
**Move data fetching to parent:**
```tsx
// Parent fetches all data once
function ProductList() {
const { data: products } = useQuery(['products'], fetchProducts)
return (
<LegendList
data={products}
renderItem={({ item }) => (
<ProductRow name={item.name} price={item.price} imageUrl={item.image} />
)}
/>
)
}
```
**For shared values, use Zustand selectors instead of Context:**
```tsx
// Incorrect: Context causes re-render when any cart value changes
function ProductRow({ id, name }: Props) {
const { items } = useContext(CartContext)
const inCart = items.includes(id)
// ...
}
// Correct: Zustand selector only re-renders when this specific value changes
function ProductRow({ id, name }: Props) {
// use Set.has (created once at the root) instead of Array.includes()
const inCart = useCartStore((s) => s.items.has(id))
// ...
}
```
**Guidelines for list items:**
- No queries or data fetching
- No expensive computations (move to parent or memoize at parent level)
- Prefer Zustand selectors over React Context
- Minimize useState/useEffect hooks
- Pass pre-computed values as props
The goal: list items should be simple rendering functions that take props and
return JSX.

View File

@@ -0,0 +1,82 @@
---
title: Pass Primitives to List Items for Memoization
impact: HIGH
impactDescription: enables effective memo() comparison
tags: lists, performance, memo, primitives
---
## Pass Primitives to List Items for Memoization
When possible, pass only primitive values (strings, numbers, booleans) as props
to list item components. Primitives enable shallow comparison in `memo()` to
work correctly, skipping re-renders when values haven't changed.
**Incorrect (object prop requires deep comparison):**
```tsx
type User = { id: string; name: string; email: string; avatar: string }
const UserRow = memo(function UserRow({ user }: { user: User }) {
// memo() compares user by reference, not value
// If parent creates new user object, this re-renders even if data is same
return <Text>{user.name}</Text>
})
renderItem={({ item }) => <UserRow user={item} />}
```
This can still be optimized, but it is harder to memoize properly.
**Correct (primitive props enable shallow comparison):**
```tsx
const UserRow = memo(function UserRow({
id,
name,
email,
}: {
id: string
name: string
email: string
}) {
// memo() compares each primitive directly
// Re-renders only if id, name, or email actually changed
return <Text>{name}</Text>
})
renderItem={({ item }) => (
<UserRow id={item.id} name={item.name} email={item.email} />
)}
```
**Pass only what you need:**
```tsx
// Incorrect: passing entire item when you only need name
<UserRow user={item} />
// Correct: pass only the fields the component uses
<UserRow name={item.name} avatarUrl={item.avatar} />
```
**For callbacks, hoist or use item ID:**
```tsx
// Incorrect: inline function creates new reference
<UserRow name={item.name} onPress={() => handlePress(item.id)} />
// Correct: pass ID, handle in child
<UserRow id={item.id} name={item.name} />
const UserRow = memo(function UserRow({ id, name }: Props) {
const handlePress = useCallback(() => {
// use id here
}, [id])
return <Pressable onPress={handlePress}><Text>{name}</Text></Pressable>
})
```
Primitive props make memoization predictable and effective.
**Note:** If you have the React Compiler enabled, you do not need to use
`memo()` or `useCallback()`, but the object references still apply.

View File

@@ -0,0 +1,104 @@
---
title: Use Item Types for Heterogeneous Lists
impact: HIGH
impactDescription: efficient recycling, less layout thrashing
tags: list, performance, recycling, heterogeneous, LegendList
---
## Use Item Types for Heterogeneous Lists
When a list has different item layouts (messages, images, headers, etc.), use a
`type` field on each item and provide `getItemType` to the list. This puts items
into separate recycling pools so a message component never gets recycled into an
image component.
**Incorrect (single component with conditionals):**
```tsx
type Item = { id: string; text?: string; imageUrl?: string; isHeader?: boolean }
function ListItem({ item }: { item: Item }) {
if (item.isHeader) {
return <HeaderItem title={item.text} />
}
if (item.imageUrl) {
return <ImageItem url={item.imageUrl} />
}
return <MessageItem text={item.text} />
}
function Feed({ items }: { items: Item[] }) {
return (
<LegendList
data={items}
renderItem={({ item }) => <ListItem item={item} />}
recycleItems
/>
)
}
```
**Correct (typed items with separate components):**
```tsx
type HeaderItem = { id: string; type: 'header'; title: string }
type MessageItem = { id: string; type: 'message'; text: string }
type ImageItem = { id: string; type: 'image'; url: string }
type FeedItem = HeaderItem | MessageItem | ImageItem
function Feed({ items }: { items: FeedItem[] }) {
return (
<LegendList
data={items}
keyExtractor={(item) => item.id}
getItemType={(item) => item.type}
renderItem={({ item }) => {
switch (item.type) {
case 'header':
return <SectionHeader title={item.title} />
case 'message':
return <MessageRow text={item.text} />
case 'image':
return <ImageRow url={item.url} />
}
}}
recycleItems
/>
)
}
```
**Why this matters:**
- **Recycling efficiency**: Items with the same type share a recycling pool
- **No layout thrashing**: A header never recycles into an image cell
- **Type safety**: TypeScript can narrow the item type in each branch
- **Better size estimation**: Use `getEstimatedItemSize` with `itemType` for
accurate estimates per type
```tsx
<LegendList
data={items}
keyExtractor={(item) => item.id}
getItemType={(item) => item.type}
getEstimatedItemSize={(index, item, itemType) => {
switch (itemType) {
case 'header':
return 48
case 'message':
return 72
case 'image':
return 300
default:
return 72
}
}}
renderItem={({ item }) => {
/* ... */
}}
recycleItems
/>
```
Reference:
[LegendList getItemType](https://legendapp.com/open-source/list/api/props/#getitemtype-v2)

View File

@@ -0,0 +1,67 @@
---
title: Use a List Virtualizer for Any List
impact: HIGH
impactDescription: reduced memory, faster mounts
tags: lists, performance, virtualization, scrollview
---
## Use a List Virtualizer for Any List
Use a list virtualizer like LegendList or FlashList instead of ScrollView with
mapped children—even for short lists. Virtualizers only render visible items,
reducing memory usage and mount time. ScrollView renders all children upfront,
which gets expensive quickly.
**Incorrect (ScrollView renders all items at once):**
```tsx
function Feed({ items }: { items: Item[] }) {
return (
<ScrollView>
{items.map((item) => (
<ItemCard key={item.id} item={item} />
))}
</ScrollView>
)
}
// 50 items = 50 components mounted, even if only 10 visible
```
**Correct (virtualizer renders only visible items):**
```tsx
import { LegendList } from '@legendapp/list'
function Feed({ items }: { items: Item[] }) {
return (
<LegendList
data={items}
// if you aren't using React Compiler, wrap these with useCallback
renderItem={({ item }) => <ItemCard item={item} />}
keyExtractor={(item) => item.id}
estimatedItemSize={80}
/>
)
}
// Only ~10-15 visible items mounted at a time
```
**Alternative (FlashList):**
```tsx
import { FlashList } from '@shopify/flash-list'
function Feed({ items }: { items: Item[] }) {
return (
<FlashList
data={items}
// if you aren't using React Compiler, wrap these with useCallback
renderItem={({ item }) => <ItemCard item={item} />}
keyExtractor={(item) => item.id}
/>
)
}
```
Benefits apply to any screen with scrollable content—profiles, settings, feeds,
search results. Default to virtualization.

View File

@@ -0,0 +1,46 @@
---
title: Install Native Dependencies in App Directory
impact: CRITICAL
impactDescription: required for autolinking to work
tags: monorepo, native, autolinking, installation
---
## Install Native Dependencies in App Directory
In a monorepo, packages with native code must be installed in the native app's
directory directly. Autolinking only scans the app's `node_modules`—it won't
find native dependencies installed in other packages.
**Incorrect (native dep in shared package only):**
```
packages/
ui/
package.json # has react-native-reanimated
app/
package.json # missing react-native-reanimated
```
Autolinking fails—native code not linked.
**Correct (native dep in app directory):**
```
packages/
ui/
package.json # has react-native-reanimated
app/
package.json # also has react-native-reanimated
```
```json
// packages/app/package.json
{
"dependencies": {
"react-native-reanimated": "3.16.1"
}
}
```
Even if the shared package uses the native dependency, the app must also list it
for autolinking to detect and link the native code.

View File

@@ -0,0 +1,63 @@
---
title: Use Single Dependency Versions Across Monorepo
impact: MEDIUM
impactDescription: avoids duplicate bundles, version conflicts
tags: monorepo, dependencies, installation
---
## Use Single Dependency Versions Across Monorepo
Use a single version of each dependency across all packages in your monorepo.
Prefer exact versions over ranges. Multiple versions cause duplicate code in
bundles, runtime conflicts, and inconsistent behavior across packages.
Use a tool like syncpack to enforce this. As a last resort, use yarn resolutions
or npm overrides.
**Incorrect (version ranges, multiple versions):**
```json
// packages/app/package.json
{
"dependencies": {
"react-native-reanimated": "^3.0.0"
}
}
// packages/ui/package.json
{
"dependencies": {
"react-native-reanimated": "^3.5.0"
}
}
```
**Correct (exact versions, single source of truth):**
```json
// package.json (root)
{
"pnpm": {
"overrides": {
"react-native-reanimated": "3.16.1"
}
}
}
// packages/app/package.json
{
"dependencies": {
"react-native-reanimated": "3.16.1"
}
}
// packages/ui/package.json
{
"dependencies": {
"react-native-reanimated": "3.16.1"
}
}
```
Use your package manager's override/resolution feature to enforce versions at
the root. When adding dependencies, specify exact versions without `^` or `~`.

View File

@@ -0,0 +1,188 @@
---
title: Use Native Navigators for Navigation
impact: HIGH
impactDescription: native performance, platform-appropriate UI
tags: navigation, react-navigation, expo-router, native-stack, tabs
---
## Use Native Navigators for Navigation
Always use native navigators instead of JS-based ones. Native navigators use
platform APIs (UINavigationController on iOS, Fragment on Android) for better
performance and native behavior.
**For stacks:** Use `@react-navigation/native-stack` or expo-router's default
stack (which uses native-stack). Avoid `@react-navigation/stack`.
**For tabs:** Use `react-native-bottom-tabs` (native) or expo-router's native
tabs. Avoid `@react-navigation/bottom-tabs` when native feel matters.
### Stack Navigation
**Incorrect (JS stack navigator):**
```tsx
import { createStackNavigator } from '@react-navigation/stack'
const Stack = createStackNavigator()
function App() {
return (
<Stack.Navigator>
<Stack.Screen name='Home' component={HomeScreen} />
<Stack.Screen name='Details' component={DetailsScreen} />
</Stack.Navigator>
)
}
```
**Correct (native stack with react-navigation):**
```tsx
import { createNativeStackNavigator } from '@react-navigation/native-stack'
const Stack = createNativeStackNavigator()
function App() {
return (
<Stack.Navigator>
<Stack.Screen name='Home' component={HomeScreen} />
<Stack.Screen name='Details' component={DetailsScreen} />
</Stack.Navigator>
)
}
```
**Correct (expo-router uses native stack by default):**
```tsx
// app/_layout.tsx
import { Stack } from 'expo-router'
export default function Layout() {
return <Stack />
}
```
### Tab Navigation
**Incorrect (JS bottom tabs):**
```tsx
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
const Tab = createBottomTabNavigator()
function App() {
return (
<Tab.Navigator>
<Tab.Screen name='Home' component={HomeScreen} />
<Tab.Screen name='Settings' component={SettingsScreen} />
</Tab.Navigator>
)
}
```
**Correct (native bottom tabs with react-navigation):**
```tsx
import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation'
const Tab = createNativeBottomTabNavigator()
function App() {
return (
<Tab.Navigator>
<Tab.Screen
name='Home'
component={HomeScreen}
options={{
tabBarIcon: () => ({ sfSymbol: 'house' }),
}}
/>
<Tab.Screen
name='Settings'
component={SettingsScreen}
options={{
tabBarIcon: () => ({ sfSymbol: 'gear' }),
}}
/>
</Tab.Navigator>
)
}
```
**Correct (expo-router native tabs):**
```tsx
// app/(tabs)/_layout.tsx
import { NativeTabs } from 'expo-router/unstable-native-tabs'
export default function TabLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name='index'>
<NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Icon sf='house.fill' md='home' />
</NativeTabs.Trigger>
<NativeTabs.Trigger name='settings'>
<NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Icon sf='gear' md='settings' />
</NativeTabs.Trigger>
</NativeTabs>
)
}
```
On iOS, native tabs automatically enable `contentInsetAdjustmentBehavior` on the
first `ScrollView` at the root of each tab screen, so content scrolls correctly
behind the translucent tab bar. If you need to disable this, use
`disableAutomaticContentInsets` on the trigger.
### Prefer Native Header Options Over Custom Components
**Incorrect (custom header component):**
```tsx
<Stack.Screen
name='Profile'
component={ProfileScreen}
options={{
header: () => <CustomHeader title='Profile' />,
}}
/>
```
**Correct (native header options):**
```tsx
<Stack.Screen
name='Profile'
component={ProfileScreen}
options={{
title: 'Profile',
headerLargeTitleEnabled: true,
headerSearchBarOptions: {
placeholder: 'Search',
},
}}
/>
```
Native headers support iOS large titles, search bars, blur effects, and proper
safe area handling automatically.
### Why Native Navigators
- **Performance**: Native transitions and gestures run on the UI thread
- **Platform behavior**: Automatic iOS large titles, Android material design
- **System integration**: Scroll-to-top on tab tap, PiP avoidance, proper safe
areas
- **Accessibility**: Platform accessibility features work automatically
Reference:
- [React Navigation Native Stack](https://reactnavigation.org/docs/native-stack-navigator)
- [React Native Bottom Tabs with React Navigation](https://oss.callstack.com/react-native-bottom-tabs/docs/guides/usage-with-react-navigation)
- [React Native Bottom Tabs with Expo Router](https://oss.callstack.com/react-native-bottom-tabs/docs/guides/usage-with-expo-router)
- [Expo Router Native Tabs](https://docs.expo.dev/router/advanced/native-tabs)

View File

@@ -0,0 +1,50 @@
---
title: Destructure Functions Early in Render (React Compiler)
impact: HIGH
impactDescription: stable references, fewer re-renders
tags: rerender, hooks, performance, react-compiler
---
## Destructure Functions Early in Render
This rule is only applicable if you are using the React Compiler.
Destructure functions from hooks at the top of render scope. Never dot into
objects to call functions. Destructured functions are stable references; dotting
creates new references and breaks memoization.
**Incorrect (dotting into object):**
```tsx
import { useRouter } from 'expo-router'
function SaveButton(props) {
const router = useRouter()
// bad: react-compiler will key the cache on "props" and "router", which are objects that change each render
const handlePress = () => {
props.onSave()
router.push('/success') // unstable reference
}
return <Button onPress={handlePress}>Save</Button>
}
```
**Correct (destructure early):**
```tsx
import { useRouter } from 'expo-router'
function SaveButton({ onSave }) {
const { push } = useRouter()
// good: react-compiler will key on push and onSave
const handlePress = () => {
onSave()
push('/success') // stable reference
}
return <Button onPress={handlePress}>Save</Button>
}
```

View File

@@ -0,0 +1,48 @@
---
title: Use .get() and .set() for Reanimated Shared Values (not .value)
impact: LOW
impactDescription: required for React Compiler compatibility
tags: reanimated, react-compiler, shared-values
---
## Use .get() and .set() for Shared Values with React Compiler
With React Compiler enabled, use `.get()` and `.set()` instead of reading or
writing `.value` directly on Reanimated shared values. The compiler can't track
property access—explicit methods ensure correct behavior.
**Incorrect (breaks with React Compiler):**
```tsx
import { useSharedValue } from 'react-native-reanimated'
function Counter() {
const count = useSharedValue(0)
const increment = () => {
count.value = count.value + 1 // opts out of react compiler
}
return <Button onPress={increment} title={`Count: ${count.value}`} />
}
```
**Correct (React Compiler compatible):**
```tsx
import { useSharedValue } from 'react-native-reanimated'
function Counter() {
const count = useSharedValue(0)
const increment = () => {
count.set(count.get() + 1)
}
return <Button onPress={increment} title={`Count: ${count.get()}`} />
}
```
See the
[Reanimated docs](https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support)
for more.

View File

@@ -0,0 +1,91 @@
---
title: useState Dispatch updaters for State That Depends on Current Value
impact: MEDIUM
impactDescription: avoids stale closures, prevents unnecessary re-renders
tags: state, hooks, useState, callbacks
---
## Use Dispatch Updaters for State That Depends on Current Value
When the next state depends on the current state, use a dispatch updater
(`setState(prev => ...)`) instead of reading the state variable directly in a
callback. This avoids stale closures and ensures you're comparing against the
latest value.
**Incorrect (reads state directly):**
```tsx
const [size, setSize] = useState<Size | undefined>(undefined)
const onLayout = (e: LayoutChangeEvent) => {
const { width, height } = e.nativeEvent.layout
// size may be stale in this closure
if (size?.width !== width || size?.height !== height) {
setSize({ width, height })
}
}
```
**Correct (dispatch updater):**
```tsx
const [size, setSize] = useState<Size | undefined>(undefined)
const onLayout = (e: LayoutChangeEvent) => {
const { width, height } = e.nativeEvent.layout
setSize((prev) => {
if (prev?.width === width && prev?.height === height) return prev
return { width, height }
})
}
```
Returning the previous value from the updater skips the re-render.
For primitive states, you don't need to compare values before firing a
re-render.
**Incorrect (unnecessary comparison for primitive state):**
```tsx
const [size, setSize] = useState<Size | undefined>(undefined)
const onLayout = (e: LayoutChangeEvent) => {
const { width, height } = e.nativeEvent.layout
setSize((prev) => (prev === width ? prev : width))
}
```
**Correct (sets primitive state directly):**
```tsx
const [size, setSize] = useState<Size | undefined>(undefined)
const onLayout = (e: LayoutChangeEvent) => {
const { width, height } = e.nativeEvent.layout
setSize(width)
}
```
However, if the next state depends on the current state, you should still use a
dispatch updater.
**Incorrect (reads state directly from the callback):**
```tsx
const [count, setCount] = useState(0)
const onTap = () => {
setCount(count + 1)
}
```
**Correct (dispatch updater):**
```tsx
const [count, setCount] = useState(0)
const onTap = () => {
setCount((prev) => prev + 1)
}
```

View File

@@ -0,0 +1,56 @@
---
title: Use fallback state instead of initialState
impact: MEDIUM
impactDescription: reactive fallbacks without syncing
tags: state, hooks, derived-state, props, initialState
---
## Use fallback state instead of initialState
Use `undefined` as initial state and nullish coalescing (`??`) to fall back to
parent or server values. State represents user intent only—`undefined` means
"user hasn't chosen yet." This enables reactive fallbacks that update when the
source changes, not just on initial render.
**Incorrect (syncs state, loses reactivity):**
```tsx
type Props = { fallbackEnabled: boolean }
function Toggle({ fallbackEnabled }: Props) {
const [enabled, setEnabled] = useState(defaultEnabled)
// If fallbackEnabled changes, state is stale
// State mixes user intent with default value
return <Switch value={enabled} onValueChange={setEnabled} />
}
```
**Correct (state is user intent, reactive fallback):**
```tsx
type Props = { fallbackEnabled: boolean }
function Toggle({ fallbackEnabled }: Props) {
const [_enabled, setEnabled] = useState<boolean | undefined>(undefined)
const enabled = _enabled ?? defaultEnabled
// undefined = user hasn't touched it, falls back to prop
// If defaultEnabled changes, component reflects it
// Once user interacts, their choice persists
return <Switch value={enabled} onValueChange={setEnabled} />
}
```
**With server data:**
```tsx
function ProfileForm({ data }: { data: User }) {
const [_theme, setTheme] = useState<string | undefined>(undefined)
const theme = _theme ?? data.theme
// Shows server value until user overrides
// Server refetch updates the fallback automatically
return <ThemePicker value={theme} onChange={setTheme} />
}
```

View File

@@ -0,0 +1,65 @@
---
title: Minimize State Variables and Derive Values
impact: MEDIUM
impactDescription: fewer re-renders, less state drift
tags: state, derived-state, hooks, optimization
---
## Minimize State Variables and Derive Values
Use the fewest state variables possible. If a value can be computed from existing state or props, derive it during render instead of storing it in state. Redundant state causes unnecessary re-renders and can drift out of sync.
**Incorrect (redundant state):**
```tsx
function Cart({ items }: { items: Item[] }) {
const [total, setTotal] = useState(0)
const [itemCount, setItemCount] = useState(0)
useEffect(() => {
setTotal(items.reduce((sum, item) => sum + item.price, 0))
setItemCount(items.length)
}, [items])
return (
<View>
<Text>{itemCount} items</Text>
<Text>Total: ${total}</Text>
</View>
)
}
```
**Correct (derived values):**
```tsx
function Cart({ items }: { items: Item[] }) {
const total = items.reduce((sum, item) => sum + item.price, 0)
const itemCount = items.length
return (
<View>
<Text>{itemCount} items</Text>
<Text>Total: ${total}</Text>
</View>
)
}
```
**Another example:**
```tsx
// Incorrect: storing both firstName, lastName, AND fullName
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [fullName, setFullName] = useState('')
// Correct: derive fullName
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const fullName = `${firstName} ${lastName}`
```
State should be the minimal source of truth. Everything else is derived.
Reference: [Choosing the State Structure](https://react.dev/learn/choosing-the-state-structure)

View File

@@ -0,0 +1,74 @@
---
title: Never Use && with Potentially Falsy Values
impact: CRITICAL
impactDescription: prevents production crash
tags: rendering, conditional, jsx, crash
---
## Never Use && with Potentially Falsy Values
Never use `{value && <Component />}` when `value` could be an empty string or
`0`. These are falsy but JSX-renderable—React Native will try to render them as
text outside a `<Text>` component, causing a hard crash in production.
**Incorrect (crashes if count is 0 or name is ""):**
```tsx
function Profile({ name, count }: { name: string; count: number }) {
return (
<View>
{name && <Text>{name}</Text>}
{count && <Text>{count} items</Text>}
</View>
)
}
// If name="" or count=0, renders the falsy value → crash
```
**Correct (ternary with null):**
```tsx
function Profile({ name, count }: { name: string; count: number }) {
return (
<View>
{name ? <Text>{name}</Text> : null}
{count ? <Text>{count} items</Text> : null}
</View>
)
}
```
**Correct (explicit boolean coercion):**
```tsx
function Profile({ name, count }: { name: string; count: number }) {
return (
<View>
{!!name && <Text>{name}</Text>}
{!!count && <Text>{count} items</Text>}
</View>
)
}
```
**Best (early return):**
```tsx
function Profile({ name, count }: { name: string; count: number }) {
if (!name) return null
return (
<View>
<Text>{name}</Text>
{count > 0 ? <Text>{count} items</Text> : null}
</View>
)
}
```
Early returns are clearest. When using conditionals inline, prefer ternary or
explicit boolean checks.
**Lint rule:** Enable `react/jsx-no-leaked-render` from
[eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-leaked-render.md)
to catch this automatically.

View File

@@ -0,0 +1,36 @@
---
title: Wrap Strings in Text Components
impact: CRITICAL
impactDescription: prevents runtime crash
tags: rendering, text, core
---
## Wrap Strings in Text Components
Strings must be rendered inside `<Text>`. React Native crashes if a string is a
direct child of `<View>`.
**Incorrect (crashes):**
```tsx
import { View } from 'react-native'
function Greeting({ name }: { name: string }) {
return <View>Hello, {name}!</View>
}
// Error: Text strings must be rendered within a <Text> component.
```
**Correct:**
```tsx
import { View, Text } from 'react-native'
function Greeting({ name }: { name: string }) {
return (
<View>
<Text>Hello, {name}!</Text>
</View>
)
}
```

View File

@@ -0,0 +1,82 @@
---
title: Never Track Scroll Position in useState
impact: HIGH
impactDescription: prevents render thrashing during scroll
tags: scroll, performance, reanimated, useRef
---
## Never Track Scroll Position in useState
Never store scroll position in `useState`. Scroll events fire rapidly—state
updates cause render thrashing and dropped frames. Use a Reanimated shared value
for animations or a ref for non-reactive tracking.
**Incorrect (useState causes jank):**
```tsx
import { useState } from 'react'
import {
ScrollView,
NativeSyntheticEvent,
NativeScrollEvent,
} from 'react-native'
function Feed() {
const [scrollY, setScrollY] = useState(0)
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
setScrollY(e.nativeEvent.contentOffset.y) // re-renders on every frame
}
return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />
}
```
**Correct (Reanimated for animations):**
```tsx
import Animated, {
useSharedValue,
useAnimatedScrollHandler,
} from 'react-native-reanimated'
function Feed() {
const scrollY = useSharedValue(0)
const onScroll = useAnimatedScrollHandler({
onScroll: (e) => {
scrollY.value = e.contentOffset.y // runs on UI thread, no re-render
},
})
return (
<Animated.ScrollView
onScroll={onScroll}
// higher number has better performance, but it fires less often.
// unset this if you need higher precision over performance.
scrollEventThrottle={16}
/>
)
}
```
**Correct (ref for non-reactive tracking):**
```tsx
import { useRef } from 'react'
import {
ScrollView,
NativeSyntheticEvent,
NativeScrollEvent,
} from 'react-native'
function Feed() {
const scrollY = useRef(0)
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
scrollY.current = e.nativeEvent.contentOffset.y // no re-render
}
return <ScrollView onScroll={onScroll} scrollEventThrottle={16} />
}
```

View File

@@ -0,0 +1,80 @@
---
title: State Must Represent Ground Truth
impact: HIGH
impactDescription: cleaner logic, easier debugging, single source of truth
tags: state, derived-state, reanimated, hooks
---
## State Must Represent Ground Truth
State variables—both React `useState` and Reanimated shared values—should
represent the actual state of something (e.g., `pressed`, `progress`, `isOpen`),
not derived visual values (e.g., `scale`, `opacity`, `translateY`). Derive
visual values from state using computation or interpolation.
**Incorrect (storing the visual output):**
```tsx
const scale = useSharedValue(1)
const tap = Gesture.Tap()
.onBegin(() => {
scale.set(withTiming(0.95))
})
.onFinalize(() => {
scale.set(withTiming(1))
})
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.get() }],
}))
```
**Correct (storing the state, deriving the visual):**
```tsx
const pressed = useSharedValue(0) // 0 = not pressed, 1 = pressed
const tap = Gesture.Tap()
.onBegin(() => {
pressed.set(withTiming(1))
})
.onFinalize(() => {
pressed.set(withTiming(0))
})
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: interpolate(pressed.get(), [0, 1], [1, 0.95]) }],
}))
```
**Why this matters:**
State variables should represent real "state", not necessarily a desired end
result.
1. **Single source of truth** — The state (`pressed`) describes what's
happening; visuals are derived
2. **Easier to extend** — Adding opacity, rotation, or other effects just
requires more interpolations from the same state
3. **Debugging** — Inspecting `pressed = 1` is clearer than `scale = 0.95`
4. **Reusable logic** — The same `pressed` value can drive multiple visual
properties
**Same principle for React state:**
```tsx
// Incorrect: storing derived values
const [isExpanded, setIsExpanded] = useState(false)
const [height, setHeight] = useState(0)
useEffect(() => {
setHeight(isExpanded ? 200 : 0)
}, [isExpanded])
// Correct: derive from state
const [isExpanded, setIsExpanded] = useState(false)
const height = isExpanded ? 200 : 0
```
State is the minimal truth. Everything else is derived.

View File

@@ -0,0 +1,66 @@
---
title: Use expo-image for Optimized Images
impact: HIGH
impactDescription: memory efficiency, caching, blurhash placeholders, progressive loading
tags: images, performance, expo-image, ui
---
## Use expo-image for Optimized Images
Use `expo-image` instead of React Native's `Image`. It provides memory-efficient caching, blurhash placeholders, progressive loading, and better performance for lists.
**Incorrect (React Native Image):**
```tsx
import { Image } from 'react-native'
function Avatar({ url }: { url: string }) {
return <Image source={{ uri: url }} style={styles.avatar} />
}
```
**Correct (expo-image):**
```tsx
import { Image } from 'expo-image'
function Avatar({ url }: { url: string }) {
return <Image source={{ uri: url }} style={styles.avatar} />
}
```
**With blurhash placeholder:**
```tsx
<Image
source={{ uri: url }}
placeholder={{ blurhash: 'LGF5]+Yk^6#M@-5c,1J5@[or[Q6.' }}
contentFit="cover"
transition={200}
style={styles.image}
/>
```
**With priority and caching:**
```tsx
<Image
source={{ uri: url }}
priority="high"
cachePolicy="memory-disk"
style={styles.hero}
/>
```
**Key props:**
- `placeholder` — Blurhash or thumbnail while loading
- `contentFit``cover`, `contain`, `fill`, `scale-down`
- `transition` — Fade-in duration (ms)
- `priority``low`, `normal`, `high`
- `cachePolicy``memory`, `disk`, `memory-disk`, `none`
- `recyclingKey` — Unique key for list recycling
For cross-platform (web + native), use `SolitoImage` from `solito/image` which uses `expo-image` under the hood.
Reference: [expo-image](https://docs.expo.dev/versions/latest/sdk/image/)

View File

@@ -0,0 +1,104 @@
---
title: Use Galeria for Image Galleries and Lightbox
impact: MEDIUM
impactDescription:
native shared element transitions, pinch-to-zoom, pan-to-close
tags: images, gallery, lightbox, expo-image, ui
---
## Use Galeria for Image Galleries and Lightbox
For image galleries with lightbox (tap to fullscreen), use `@nandorojo/galeria`.
It provides native shared element transitions with pinch-to-zoom, double-tap
zoom, and pan-to-close. Works with any image component including `expo-image`.
**Incorrect (custom modal implementation):**
```tsx
function ImageGallery({ urls }: { urls: string[] }) {
const [selected, setSelected] = useState<string | null>(null)
return (
<>
{urls.map((url) => (
<Pressable key={url} onPress={() => setSelected(url)}>
<Image source={{ uri: url }} style={styles.thumbnail} />
</Pressable>
))}
<Modal visible={!!selected} onRequestClose={() => setSelected(null)}>
<Image source={{ uri: selected! }} style={styles.fullscreen} />
</Modal>
</>
)
}
```
**Correct (Galeria with expo-image):**
```tsx
import { Galeria } from '@nandorojo/galeria'
import { Image } from 'expo-image'
function ImageGallery({ urls }: { urls: string[] }) {
return (
<Galeria urls={urls}>
{urls.map((url, index) => (
<Galeria.Image index={index} key={url}>
<Image source={{ uri: url }} style={styles.thumbnail} />
</Galeria.Image>
))}
</Galeria>
)
}
```
**Single image:**
```tsx
import { Galeria } from '@nandorojo/galeria'
import { Image } from 'expo-image'
function Avatar({ url }: { url: string }) {
return (
<Galeria urls={[url]}>
<Galeria.Image>
<Image source={{ uri: url }} style={styles.avatar} />
</Galeria.Image>
</Galeria>
)
}
```
**With low-res thumbnails and high-res fullscreen:**
```tsx
<Galeria urls={highResUrls}>
{lowResUrls.map((url, index) => (
<Galeria.Image index={index} key={url}>
<Image source={{ uri: url }} style={styles.thumbnail} />
</Galeria.Image>
))}
</Galeria>
```
**With FlashList:**
```tsx
<Galeria urls={urls}>
<FlashList
data={urls}
renderItem={({ item, index }) => (
<Galeria.Image index={index}>
<Image source={{ uri: item }} style={styles.thumbnail} />
</Galeria.Image>
)}
numColumns={3}
estimatedItemSize={100}
/>
</Galeria>
```
Works with `expo-image`, `SolitoImage`, `react-native` Image, or any image
component.
Reference: [Galeria](https://github.com/nandorojo/galeria)

View File

@@ -0,0 +1,78 @@
---
title: Measuring View Dimensions
impact: MEDIUM
impactDescription: synchronous measurement, avoid unnecessary re-renders
tags: layout, measurement, onLayout, useLayoutEffect
---
## Measuring View Dimensions
Use both `useLayoutEffect` (synchronous) and `onLayout` (for updates). The sync
measurement gives you the initial size immediately; `onLayout` keeps it current
when the view changes. For non-primitive states, use a dispatch updater to
compare values and avoid unnecessary re-renders.
**Height only:**
```tsx
import { useLayoutEffect, useRef, useState } from 'react'
import { View, LayoutChangeEvent } from 'react-native'
function MeasuredBox({ children }: { children: React.ReactNode }) {
const ref = useRef<View>(null)
const [height, setHeight] = useState<number | undefined>(undefined)
useLayoutEffect(() => {
// Sync measurement on mount (RN 0.82+)
const rect = ref.current?.getBoundingClientRect()
if (rect) setHeight(rect.height)
// Pre-0.82: ref.current?.measure((x, y, w, h) => setHeight(h))
}, [])
const onLayout = (e: LayoutChangeEvent) => {
setHeight(e.nativeEvent.layout.height)
}
return (
<View ref={ref} onLayout={onLayout}>
{children}
</View>
)
}
```
**Both dimensions:**
```tsx
import { useLayoutEffect, useRef, useState } from 'react'
import { View, LayoutChangeEvent } from 'react-native'
type Size = { width: number; height: number }
function MeasuredBox({ children }: { children: React.ReactNode }) {
const ref = useRef<View>(null)
const [size, setSize] = useState<Size | undefined>(undefined)
useLayoutEffect(() => {
const rect = ref.current?.getBoundingClientRect()
if (rect) setSize({ width: rect.width, height: rect.height })
}, [])
const onLayout = (e: LayoutChangeEvent) => {
const { width, height } = e.nativeEvent.layout
setSize((prev) => {
// for non-primitive states, compare values before firing a re-render
if (prev?.width === width && prev?.height === height) return prev
return { width, height }
})
}
return (
<View ref={ref} onLayout={onLayout}>
{children}
</View>
)
}
```
Use functional setState to compare—don't read state directly in the callback.

View File

@@ -0,0 +1,174 @@
---
title: Use Native Menus for Dropdowns and Context Menus
impact: HIGH
impactDescription: native accessibility, platform-consistent UX
tags: user-interface, menus, context-menus, zeego, accessibility
---
## Use Native Menus for Dropdowns and Context Menus
Use native platform menus instead of custom JS implementations. Native menus
provide built-in accessibility, consistent platform UX, and better performance.
Use [zeego](https://zeego.dev) for cross-platform native menus.
**Incorrect (custom JS menu):**
```tsx
import { useState } from 'react'
import { View, Pressable, Text } from 'react-native'
function MyMenu() {
const [open, setOpen] = useState(false)
return (
<View>
<Pressable onPress={() => setOpen(!open)}>
<Text>Open Menu</Text>
</Pressable>
{open && (
<View style={{ position: 'absolute', top: 40 }}>
<Pressable onPress={() => console.log('edit')}>
<Text>Edit</Text>
</Pressable>
<Pressable onPress={() => console.log('delete')}>
<Text>Delete</Text>
</Pressable>
</View>
)}
</View>
)
}
```
**Correct (native menu with zeego):**
```tsx
import * as DropdownMenu from 'zeego/dropdown-menu'
function MyMenu() {
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Pressable>
<Text>Open Menu</Text>
</Pressable>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item key='edit' onSelect={() => console.log('edit')}>
<DropdownMenu.ItemTitle>Edit</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
<DropdownMenu.Item
key='delete'
destructive
onSelect={() => console.log('delete')}
>
<DropdownMenu.ItemTitle>Delete</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
)
}
```
**Context menu (long-press):**
```tsx
import * as ContextMenu from 'zeego/context-menu'
function MyContextMenu() {
return (
<ContextMenu.Root>
<ContextMenu.Trigger>
<View style={{ padding: 20 }}>
<Text>Long press me</Text>
</View>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item key='copy' onSelect={() => console.log('copy')}>
<ContextMenu.ItemTitle>Copy</ContextMenu.ItemTitle>
</ContextMenu.Item>
<ContextMenu.Item key='paste' onSelect={() => console.log('paste')}>
<ContextMenu.ItemTitle>Paste</ContextMenu.ItemTitle>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
)
}
```
**Checkbox items:**
```tsx
import * as DropdownMenu from 'zeego/dropdown-menu'
function SettingsMenu() {
const [notifications, setNotifications] = useState(true)
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Pressable>
<Text>Settings</Text>
</Pressable>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.CheckboxItem
key='notifications'
value={notifications}
onValueChange={() => setNotifications((prev) => !prev)}
>
<DropdownMenu.ItemIndicator />
<DropdownMenu.ItemTitle>Notifications</DropdownMenu.ItemTitle>
</DropdownMenu.CheckboxItem>
</DropdownMenu.Content>
</DropdownMenu.Root>
)
}
```
**Submenus:**
```tsx
import * as DropdownMenu from 'zeego/dropdown-menu'
function MenuWithSubmenu() {
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Pressable>
<Text>Options</Text>
</Pressable>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item key='home' onSelect={() => console.log('home')}>
<DropdownMenu.ItemTitle>Home</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger key='more'>
<DropdownMenu.ItemTitle>More Options</DropdownMenu.ItemTitle>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent>
<DropdownMenu.Item key='settings'>
<DropdownMenu.ItemTitle>Settings</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
<DropdownMenu.Item key='help'>
<DropdownMenu.ItemTitle>Help</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
</DropdownMenu.Content>
</DropdownMenu.Root>
)
}
```
Reference: [Zeego Documentation](https://zeego.dev/components/dropdown-menu)

View File

@@ -0,0 +1,77 @@
---
title: Use Native Modals Over JS-Based Bottom Sheets
impact: HIGH
impactDescription: native performance, gestures, accessibility
tags: modals, bottom-sheet, native, react-navigation
---
## Use Native Modals Over JS-Based Bottom Sheets
Use native `<Modal>` with `presentationStyle="formSheet"` or React Navigation
v7's native form sheet instead of JS-based bottom sheet libraries. Native modals
have built-in gestures, accessibility, and better performance. Rely on native UI
for low-level primitives.
**Incorrect (JS-based bottom sheet):**
```tsx
import BottomSheet from 'custom-js-bottom-sheet'
function MyScreen() {
const sheetRef = useRef<BottomSheet>(null)
return (
<View style={{ flex: 1 }}>
<Button onPress={() => sheetRef.current?.expand()} title='Open' />
<BottomSheet ref={sheetRef} snapPoints={['50%', '90%']}>
<View>
<Text>Sheet content</Text>
</View>
</BottomSheet>
</View>
)
}
```
**Correct (native Modal with formSheet):**
```tsx
import { Modal, View, Text, Button } from 'react-native'
function MyScreen() {
const [visible, setVisible] = useState(false)
return (
<View style={{ flex: 1 }}>
<Button onPress={() => setVisible(true)} title='Open' />
<Modal
visible={visible}
presentationStyle='formSheet'
animationType='slide'
onRequestClose={() => setVisible(false)}
>
<View>
<Text>Sheet content</Text>
</View>
</Modal>
</View>
)
}
```
**Correct (React Navigation v7 native form sheet):**
```tsx
// In your navigator
<Stack.Screen
name='Details'
component={DetailsScreen}
options={{
presentation: 'formSheet',
sheetAllowedDetents: 'fitToContents',
}}
/>
```
Native modals provide swipe-to-dismiss, proper keyboard avoidance, and
accessibility out of the box.

View File

@@ -0,0 +1,61 @@
---
title: Use Pressable Instead of Touchable Components
impact: LOW
impactDescription: modern API, more flexible
tags: ui, pressable, touchable, gestures
---
## Use Pressable Instead of Touchable Components
Never use `TouchableOpacity` or `TouchableHighlight`. Use `Pressable` from
`react-native` or `react-native-gesture-handler` instead.
**Incorrect (legacy Touchable components):**
```tsx
import { TouchableOpacity } from 'react-native'
function MyButton({ onPress }: { onPress: () => void }) {
return (
<TouchableOpacity onPress={onPress} activeOpacity={0.7}>
<Text>Press me</Text>
</TouchableOpacity>
)
}
```
**Correct (Pressable):**
```tsx
import { Pressable } from 'react-native'
function MyButton({ onPress }: { onPress: () => void }) {
return (
<Pressable onPress={onPress}>
<Text>Press me</Text>
</Pressable>
)
}
```
**Correct (Pressable from gesture handler for lists):**
```tsx
import { Pressable } from 'react-native-gesture-handler'
function ListItem({ onPress }: { onPress: () => void }) {
return (
<Pressable onPress={onPress}>
<Text>Item</Text>
</Pressable>
)
}
```
Use `react-native-gesture-handler` Pressable inside scrollable lists for better
gesture coordination, as long as you are using the ScrollView from
`react-native-gesture-handler` as well.
**For animated press states (scale, opacity changes):** Use `GestureDetector`
with Reanimated shared values instead of Pressable's style callback. See the
`animation-gesture-detector-press` rule.

View File

@@ -0,0 +1,65 @@
---
title: Use contentInsetAdjustmentBehavior for Safe Areas
impact: MEDIUM
impactDescription: native safe area handling, no layout shifts
tags: safe-area, scrollview, layout
---
## Use contentInsetAdjustmentBehavior for Safe Areas
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.
**Incorrect (SafeAreaView wrapper):**
```tsx
import { SafeAreaView, ScrollView, View, Text } from 'react-native'
function MyScreen() {
return (
<SafeAreaView style={{ flex: 1 }}>
<ScrollView>
<View>
<Text>Content</Text>
</View>
</ScrollView>
</SafeAreaView>
)
}
```
**Incorrect (manual safe area padding):**
```tsx
import { ScrollView, View, Text } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
function MyScreen() {
const insets = useSafeAreaInsets()
return (
<ScrollView contentContainerStyle={{ paddingTop: insets.top }}>
<View>
<Text>Content</Text>
</View>
</ScrollView>
)
}
```
**Correct (native content inset adjustment):**
```tsx
import { ScrollView, View, Text } from 'react-native'
function MyScreen() {
return (
<ScrollView contentInsetAdjustmentBehavior='automatic'>
<View>
<Text>Content</Text>
</View>
</ScrollView>
)
}
```
The native approach handles dynamic safe areas (keyboard, toolbars) and allows content to scroll behind the status bar naturally.

View File

@@ -0,0 +1,45 @@
---
title: Use contentInset for Dynamic ScrollView Spacing
impact: LOW
impactDescription: smoother updates, no layout recalculation
tags: scrollview, layout, contentInset, performance
---
## Use contentInset for Dynamic ScrollView Spacing
When adding space to the top or bottom of a ScrollView that may change
(keyboard, toolbars, dynamic content), use `contentInset` instead of padding.
Changing `contentInset` doesn't trigger layout recalculation—it adjusts the
scroll area without re-rendering content.
**Incorrect (padding causes layout recalculation):**
```tsx
function Feed({ bottomOffset }: { bottomOffset: number }) {
return (
<ScrollView contentContainerStyle={{ paddingBottom: bottomOffset }}>
{children}
</ScrollView>
)
}
// Changing bottomOffset triggers full layout recalculation
```
**Correct (contentInset for dynamic spacing):**
```tsx
function Feed({ bottomOffset }: { bottomOffset: number }) {
return (
<ScrollView
contentInset={{ bottom: bottomOffset }}
scrollIndicatorInsets={{ bottom: bottomOffset }}
>
{children}
</ScrollView>
)
}
// Changing bottomOffset only adjusts scroll bounds
```
Use `scrollIndicatorInsets` alongside `contentInset` to keep the scroll
indicator aligned. For static spacing that never changes, padding is fine.

View File

@@ -0,0 +1,87 @@
---
title: Modern React Native Styling Patterns
impact: MEDIUM
impactDescription: consistent design, smoother borders, cleaner layouts
tags: styling, css, layout, shadows, gradients
---
## Modern React Native Styling Patterns
Follow these styling patterns for cleaner, more consistent React Native code.
**Always use `borderCurve: 'continuous'` with `borderRadius`:**
```tsx
// Incorrect
{ borderRadius: 12 }
// Correct smoother iOS-style corners
{ borderRadius: 12, borderCurve: 'continuous' }
```
**Use `gap` instead of margin for spacing between elements:**
```tsx
// Incorrect margin on children
<View>
<Text style={{ marginBottom: 8 }}>Title</Text>
<Text style={{ marginBottom: 8 }}>Subtitle</Text>
</View>
// Correct gap on parent
<View style={{ gap: 8 }}>
<Text>Title</Text>
<Text>Subtitle</Text>
</View>
```
**Use `padding` for space within, `gap` for space between:**
```tsx
<View style={{ padding: 16, gap: 12 }}>
<Text>First</Text>
<Text>Second</Text>
</View>
```
**Use `experimental_backgroundImage` for linear gradients:**
```tsx
// Incorrect third-party gradient library
<LinearGradient colors={['#000', '#fff']} />
// Correct native CSS gradient syntax
<View
style={{
experimental_backgroundImage: 'linear-gradient(to bottom, #000, #fff)',
}}
/>
```
**Use CSS `boxShadow` string syntax for shadows:**
```tsx
// Incorrect legacy shadow objects or elevation
{ shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1 }
{ elevation: 4 }
// Correct CSS box-shadow syntax
{ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' }
```
**Avoid multiple font sizes use weight and color for emphasis:**
```tsx
// Incorrect varying font sizes for hierarchy
<Text style={{ fontSize: 18 }}>Title</Text>
<Text style={{ fontSize: 14 }}>Subtitle</Text>
<Text style={{ fontSize: 12 }}>Caption</Text>
// Correct consistent size, vary weight and color
<Text style={{ fontWeight: '600' }}>Title</Text>
<Text style={{ color: '#666' }}>Subtitle</Text>
<Text style={{ color: '#999' }}>Caption</Text>
```
Limiting font sizes creates visual consistency. Use `fontWeight` (bold/semibold)
and grayscale colors for hierarchy instead.

View File

@@ -0,0 +1 @@
/home/localadmin/src/agent-skills/skills/vercel-react-native-skills/