1. List Views
Empty States
Every list view must include an empty state component when there is no data to display.
// ✅ Always handle empty state
<FlatList
data={items}
ListEmptyComponent={<EmptyState message={t('noItemsFound')} />}
...
/>
Skeleton Loading
Show skeleton placeholders while data is loading or being refetched — never a raw spinner inside a list.
if (isLoading) return <SkeletonList count={6} />;
Pull to Refresh
All list views that call an API must support pull-to-refresh.
<FlatList
refreshControl={
<RefreshControl refreshing={isRefetching} onRefresh={refetch} />
}
...
/>
Performance Best Practices
Apply these to all FlatList / SectionList components for smooth scrolling:
| Prop | Recommended Value |
|---|---|
keyExtractor |
Stable unique ID (not index) |
removeClippedSubviews |
true |
maxToRenderPerBatch |
10 |
windowSize |
5 |
initialNumToRender |
10 |
getItemLayout |
Provide if item height is fixed |
// ✅ Memoize renderItem to prevent unnecessary re-renders
const renderItem = useCallback(({ item }) => <ItemCard item={item} />, []);
2. Loading States
No Simultaneous Loaders
Never show multiple loading indicators at the same time. Use a single loading state per screen or section.
// ✅ Single loading gate
if (isLoading) return <ScreenSkeleton />;
Consistent Loading UI
Use the same loading component throughout the app. Agree on one pattern and stick to it:
- Screen-level:
<ScreenSkeleton /> - List-level:
<SkeletonList count={n} /> - Button-level:
<ActivityIndicator size="small" />inside the button
3. Images
All images must:
- Have a placeholder skeleton shown while loading
- Fade in over 300ms once loaded
- Handle load errors gracefully with a fallback
<FastImage
source={{ uri: imageUrl }}
defaultSource={require('@/assets/placeholder.png')}
style={styles.image}
/>
// Custom fade-in wrapper
<ImageWithFade uri={imageUrl} duration={300} />
4. Error Handling
Human-Readable Error Messages
Never surface raw API errors or technical messages to users. Map all errors to user-friendly strings via the translation system.
// errors.ts
export const getErrorMessage = (error: unknown): string => {
if (isNetworkError(error)) return i18n.t('errors.network');
if (isAuthError(error)) return i18n.t('errors.unauthorized');
return i18n.t('errors.generic');
};
Toast on Error
Show a toast notification on any API error. Do not use alerts for non-critical errors.
onError: (error) => {
showToast({ message: getErrorMessage(error), type: 'error' });
}
5. Buttons & Icons
Disabled State
Buttons and action icons must be disabled while an API request is in progress.
<Button
onPress={handleSubmit}
disabled={isLoading || isSubmitting}
loading={isLoading}
/>
Hit Slop
Any button or icon that is visually smaller than 44×44pt must include a hitSlop of at least 5.
<TouchableOpacity hitSlop={{ top: 5, bottom: 5, left: 5, right: 5 }}>
<Icon name="close" size={16} />
</TouchableOpacity>
Haptic Feedback
Add haptic feedback to primary action buttons and interactive elements where it enhances the experience.
import * as Haptics from 'expo-haptics';
const handlePress = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
onPress();
};
6. Destructive Actions
All delete, remove, or irreversible actions must show a confirmation dialog before proceeding.
// ✅ Always confirm before destructive actions
const handleDelete = () => {
showConfirmAlert({
title: t('confirm.deleteTitle'),
message: t('confirm.deleteMessage'),
confirmText: t('common.delete'),
onConfirm: () => deleteItem(id),
});
};
Use a custom ConfirmAlert JS component — not the native Alert.alert() — to ensure
consistent UI across platforms. See §10.
7. Separation of Concerns
API logic and UI components must not be mixed. Follow a clear layered architecture:
hooks/
useItems.ts ← API calls, state, error handling
services/
itemsService.ts ← Raw API functions
components/
ItemList.tsx ← UI only, receives data via props/hooks
// ✅ Clean separation
const ItemList = () => {
const { items, isLoading, refetch } = useItems(); // ← logic in hook
return <FlatList data={items} ... />; // ← UI only here
};
8. Constants & Environment Variables
Never hardcode keys, URLs, or environment-specific values.
// .env
API_BASE_URL=https://api.example.com
STREAM_API_KEY=your_key_here
// constants/config.ts
export const API_BASE_URL = process.env.API_BASE_URL;
export const STREAM_API_KEY = process.env.STREAM_API_KEY;
9. Translations
Every visible string must be wrapped in the translation function. No hardcoded text in components.
// ❌ Never
<Text>No results found</Text>
// ✅ Always
<Text>{t('search.noResults')}</Text>
10. Cross-Platform UI Consistency
Use custom JS components instead of native platform components for anything that affects visual consistency.
| Replace native… | With custom component |
|---|---|
| DateTimePicker (native) | <AppDatePicker /> |
Alert.alert() |
<ConfirmAlert /> / <AppAlert /> |
ActionSheetIOS |
<AppActionSheet /> |
This ensures identical appearance and behaviour on both iOS and Android.
11. UI Re-renders
Components should not re-render if nothing relevant has changed.
- Use
React.memo()for pure components - Use
useCallbackfor event handlers passed as props - Use
useMemofor expensive derived values - Keep selectors narrow when using state management
const ItemCard = React.memo(({ item }: Props) => {
return <View>...</View>;
});
12. Code Quality
Toolchain (required for all projects)
| Tool | Purpose |
|---|---|
| ESLint | JS/TS linting |
| TSLint / TypeScript strict | Type safety |
| Prettier | Code formatting |
| Husky | Git hook enforcement |
| lint-staged | Run linters only on staged files |
Pre-commit Hook
Husky must run lint and type checks before every commit. Nothing broken ships.
// .husky/pre-commit
npx lint-staged
AI-Assisted Code Review
Before committing or opening a PR, run your changes through an AI tool (e.g. Claude, Copilot) for a review pass. Ask it to:
- Check for edge cases and missing error handling
- Suggest performance improvements
- Confirm naming and structure follows project conventions
13. Release Notes
Every app release must include structured release notes, committed to the repo.
## v1.4.2
- **Version**: 1.4.2 (build 48)
- **Release date**: 2025-04-20
- **Platform**: iOS + Android
- **What's new**:
- Fixed audio session bug during livestream
- Improved skeleton loading on feed screen
- Pull-to-refresh added to notifications list
- **Notes**: Requires minimum iOS 15 / Android 10
Store these in CHANGELOG.md at the project root.
14. AI-Generated UI
When using AI tools to generate UI components, the output must match the existing project's visual language. Before generating, provide the AI with:
- Screenshots or descriptions of existing screens
- The project's design tokens (colours, spacing, typography)
- Existing component examples as reference
"Match the existing UI" is a hard requirement. Generic AI-generated UI that doesn't fit the project style is not acceptable.
Last updated: April 2025 · Maintained by the mobile team