| name | react-native-best-practices |
| description | Provides React Native performance optimization guidelines for FPS, TTI, bundle size, memory leaks, re-renders, and animations. Applies to tasks involving Hermes optimization, JS thread blocking, bridge overhead, FlashList, native modules, or debugging jank and frame drops. |
| license | MIT |
| metadata | {"author":"Callstack","tags":"react-native, expo, performance, optimization, profiling"} |
React Native Best Practices
Overview
Performance optimization guide for React Native applications, covering JavaScript/React, Native (iOS/Android), and bundling optimizations. Based on Callstack's "Ultimate Guide to React Native Optimization".
Skill Format
Each reference file follows a hybrid format for fast lookup and deep understanding:
- Quick Pattern: Incorrect/Correct code snippets for immediate pattern matching
- Quick Command: Shell commands for process/measurement skills
- Quick Config: Configuration snippets for setup-focused skills
- Quick Reference: Summary tables for conceptual skills
- Deep Dive: Full context with When to Use, Prerequisites, Step-by-Step, Common Pitfalls
Impact ratings: CRITICAL (fix immediately), HIGH (significant improvement), MEDIUM (worthwhile optimization)
When to Apply
Reference these guidelines when:
- Debugging slow/janky UI or animations
- Investigating memory leaks (JS or native)
- Optimizing app startup time (TTI)
- Reducing bundle or app size
- Writing native modules (Turbo Modules)
- Profiling React Native performance
- Reviewing React Native code for performance
Security Notes
- Treat shell commands in these references as local developer operations. Review them before running, prefer version-pinned tooling, and avoid piping remote scripts directly to a shell.
- Treat third-party libraries and plugins as dependencies that still require normal supply-chain controls: pin versions, verify provenance, and update through your standard review process.
- Treat Re.Pack code splitting as first-party artifact delivery only. Remote chunks must come from trusted HTTPS origins you control and be pinned to the current app release.
Priority-Ordered Guidelines
| Priority | Category | Impact | Prefix |
|---|
| 1 | FPS & Re-renders | CRITICAL | js-* |
| 2 | Bundle Size | CRITICAL | bundle-* |
| 3 | TTI Optimization | HIGH | native-*, bundle-* |
| 4 | Native Performance | HIGH | native-* |
| 5 | Memory Management | MEDIUM-HIGH | js-*, native-* |
| 6 | Animations | MEDIUM | js-* |
Quick Reference
Optimization Workflow
Follow this cycle for any performance issue: Measure → Optimize → Re-measure → Validate
- Measure: Capture baseline metrics (FPS, TTI, bundle size) before changes
- Optimize: Apply the targeted fix from the relevant reference
- Re-measure: Run the same measurement to get updated metrics
- Validate: Confirm improvement (e.g., FPS 45→60, TTI 3.2s→1.8s, bundle 2.1MB→1.6MB)
If metrics did not improve, revert and try the next suggested fix.
Critical: FPS & Re-renders
Profile first:
Common fixes:
- Replace ScrollView with FlatList/FlashList for lists
- Use React Compiler for automatic memoization
- Use atomic state (Jotai/Zustand) to reduce re-renders
- Use
useDeferredValue for expensive computations
Critical: Bundle Size
Analyze bundle:
npx react-native bundle \
--entry-file index.js \
--bundle-output output.js \
--platform ios \
--sourcemap-output output.js.map \
--dev false --minify true
npx source-map-explorer output.js --no-border-checks
Verify improvement after optimization:
ls -lh output.js
npx react-native bundle --entry-file index.js --bundle-output output.js \
--platform ios --dev false --minify true
ls -lh output.js
Common fixes:
- Avoid barrel imports (import directly from source)
- Remove unnecessary Intl polyfills only after checking Hermes API and method coverage
- Enable tree shaking (Expo SDK 52+ or Re.Pack)
- Enable R8 for Android native code shrinking
High: TTI Optimization
Measure TTI:
- Use
react-native-performance for markers
- Only measure cold starts (exclude warm/hot/prewarm)
Common fixes:
- Disable JS bundle compression on Android (enables Hermes mmap)
- Use native navigation (react-native-screens)
- Preload commonly-used expensive screens before navigating to them
High: Native Performance
Profile native:
- iOS: Xcode Instruments → Time Profiler
- Android: Android Studio → CPU Profiler
Common fixes:
- Use background threads for heavy native work
- Prefer async over sync Turbo Module methods
- Use C++ for cross-platform performance-critical code
References
Full documentation with code examples in references/:
JavaScript/React (js-*)
Native (native-*)
Bundling (bundle-*)
Searching References
grep -l "reanimated" references/
grep -l "flatlist" references/
grep -l "memory" references/
grep -l "profil" references/
grep -l "tti" references/
grep -l "bundle" references/
Problem → Skill Mapping
Attribution
Based on "The Ultimate Guide to React Native Optimization" by Callstack.
AcademyHub Mobile Architecture Extensions
Overview
These extensions enforce the specific architectural requirements for the AcademyHub mobile app. All references in this skill MUST comply with these patterns when working on @academyhub-mobile/.
🔑 TanStack Query Enforcement (CRITICAL)
networkMode: 'offlineFirst' Requirement
ALL TanStack Query hooks MUST include networkMode: 'offlineFirst' to enable offline-first data fetching.
export const useStudentListQuery = (params?: StudentListParams) => {
return useQuery({
queryKey: studentKeys.list(params),
queryFn: () => studentService.getList(params),
networkMode: 'offlineFirst',
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
});
};
export const useCreateStudentMutation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateStudentDto) => studentService.create(data),
networkMode: 'offlineFirst',
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: studentKeys.lists() });
},
});
};
❌ INCORRECT PATTERN
export const useStudentListQuery = (params?: StudentListParams) => {
return useQuery({
queryKey: studentKeys.list(params),
queryFn: () => studentService.getList(params),
});
};
TanStack Query + Jotai Integration
Use atomWithQuery for shared server state with offline-first mode:
import { atomWithQuery } from 'jotai/query';
export const studentListAtom = atomWithQuery(() => ({
queryKey: ['students', 'list'],
queryFn: () => studentService.getList(),
networkMode: 'offlineFirst',
staleTime: 5 * 60 * 1000,
}));
🎨 Elevated MD3 Design System (MANDATORY)
StandardCard Usage
ALWAYS use StandardCard for primary content containers with elevation=4 (default):
import { StandardCard } from '@features/_global/components/StandardCard';
<StandardCard
title="Section Title"
subtitle="Description"
elevation={4}
>
{children}
</StandardCard>
StandardListItem Usage
ALWAYS use StandardListItem for list entries with elevation=1:
import { StandardListItem } from '@features/_global/components/StandardListItem';
<StandardListItem
title={item.title}
subtitle={item.subtitle}
elevation={1}
onPress={handlePress}
/>
Surface + TouchableRipple Pattern
For custom list items, use the Surface + TouchableRipple pattern:
<Surface elevation={1} style={styles.itemSurface}>
<TouchableRipple borderless onPress={handlePress} style={styles.touchable}>
<View style={styles.content}>
{/* List item content */}
</View>
</TouchableRipple>
</Surface>
const styles = StyleSheet.create({
itemSurface: {
borderRadius: borderRadius.md,
overflow: 'hidden',
marginBottom: spacing.xs,
},
});
Design Token Enforcement
NEVER use hardcoded values. ALL styling MUST use design tokens from @core/styles:
import { colors, spacing, fontSizes, borderRadius } from '@core/styles';
const styles = StyleSheet.create({
container: {
padding: spacing.md,
backgroundColor: colors.surface,
borderRadius: borderRadius.md,
},
});
const styles = StyleSheet.create({
container: {
padding: 16,
backgroundColor: '#FFFFFF',
borderRadius: 8,
},
});
📝 Form Handling: react-hook-form + zod (MANDATORY)
ALL forms MUST use react-hook-form with zod validation schema.
Correct Pattern
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const studentFormSchema = z.object({
name: z.string().min(1, 'Nama wajib diisi'),
nis: z.string().min(4, 'NIS minimal 4 karakter'),
classId: z.number().min(1, 'Kelas wajib dipilih'),
email: z.string().email('Email tidak valid').optional(),
});
type StudentFormData = z.infer<typeof studentFormSchema>;
export const StudentFormScreen = () => {
const {
control,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm<StudentFormData>({
resolver: zodResolver(studentFormSchema),
defaultValues: {
name: '',
nis: '',
email: '',
},
});
const onSubmit = async (data: StudentFormData) => {
await createStudentMutation.mutateAsync(data);
};
return (
<FormLayout
title="Tambah Student"
footer={{
mode: 'single',
label: 'Simpan',
onPress: handleSubmit(onSubmit),
loading: isSubmitting,
}}
>
<Controller
control={control}
name="name"
render={({ field: { onChange, onBlur, value } }) => (
<TextInput
label="Nama"
value={value}
onChangeText={onChange}
onBlur={onBlur}
error={!!errors.name}
/>
)}
/>
</FormLayout>
);
};
Form Validation Rules
- ✅ Use
zod for all form validation schemas
- ✅ Use
zodResolver from @hookform/resolvers/zod
- ✅ Use
Controller from react-hook-form with React Native Paper components
- ✅ Display validation errors using
errors.{fieldName}?.message
- ✅ Handle async validation with proper loading states
❌ INCORRECT Patterns
const [name, setName] = useState('');
const [errors, setErrors] = useState<string[]>([]);
const validate = () => {
if (!name) {
setErrors(['Nama wajib diisi']);
return false;
}
return true;
};
<TextInput
value={name}
onChangeText={(text) => {
setName(text);
if (text.length < 2) {
setError('Nama terlalu pendek');
}
}}
/>
🚀 FlashList Requirement (CRITICAL)
NEVER use FlatList or ScrollView.map for lists. ALWAYS use FlashList from @shopify/flash-list.
Correct Pattern
import { FlashList } from '@shopify/flash-list';
import { StudentListItem } from '@features/student/components';
export const StudentListScreen = () => {
const { students, onLoadMore } = useStudentList();
return (
<FlashList
data={students}
renderItem={renderStudentItem}
estimatedItemSize={80} // ✅ MANDATORY
onEndReached={onLoadMore}
keyExtractor={(item) => item.id.toString()}
/>
);
};
const renderStudentItem = React.memo(({ item }: { item: Student }) => (
<StudentListItem student={item} />
));
❌ INCORRECT Patterns
import { FlatList } from 'react-native';
<ScrollView>
{students.map(student => (
<StudentListItem key={student.id} student={student} />
))}
</ScrollView>
📁 Query Module Structure (MANDATORY)
All TanStack Query modules MUST follow the three-file structure:
@core/states/queries/{module}/
├── index.ts # Barrel export
├── keys.ts # Query key constants
└── query.ts # React Query hooks
export const studentKeys = {
all: ['students'] as const,
lists: () => [...studentKeys.all, 'list'] as const,
list: (params?: StudentListParams) => [...studentKeys.lists(), params] as const,
details: () => [...studentKeys.all, 'detail'] as const,
detail: (id: string) => [...studentKeys.details(), id] as const,
};
export const useStudentListQuery = (params?: StudentListParams) => {
return useQuery({
queryKey: studentKeys.list(params),
queryFn: () => studentService.getList(params),
networkMode: 'offlineFirst',
});
};
📱 Mobile API Endpoints
Mobile apps MUST use mobile-specific API endpoints at /api/mobile/v1/ instead of /api/v1/:
| Feature | Mobile Endpoint |
|---|
| Auth | /api/mobile/v1/auth/* |
| Profile | /api/mobile/v1/profile/* |
| Canteen | /api/mobile/v1/canteen/* |
✅ Verification Commands
grep -r "networkMode: 'offlineFirst'" . --include="*.ts" --include="*.tsx"
grep -r "FlatList" . --include="*.tsx" | grep -v "node_modules"
grep -r "zodResolver" . --include="*.tsx"
grep -r "StandardCard" . --include="*.tsx"
grep -E "#[0-9A-Fa-f]{3,6}" . --include="*.tsx" | grep -v "node_modules"