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 useCallback for event handlers passed as props
  • Use useMemo for 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