expo Best Practices
This document outlines the definitive standards for Expo development. Adhere to these guidelines to ensure consistency, performance, and maintainability across our projects.
1. Code Organization and Structure
Maintain a clean, scalable project structure.
- Root Structure:
assets/: Static assets (images, fonts).
src/: All application logic.
src/components/: Reusable UI components.
src/screens/: Top-level components representing distinct app views.
src/navigation/: Navigation configuration (if not using Expo Router's app/).
src/hooks/: Custom React Hooks.
src/utils/: Utility functions, constants.
src/services/: API clients, data fetching logic.
- Naming Conventions:
- Files/Folders:
kebab-case for directories, PascalCase for components, camelCase for hooks/utils.
- Components:
PascalCase (e.g., Button.tsx, HomeScreen.tsx).
- Variables/Functions:
camelCase (e.g., userName, fetchData).
- Constants:
UPPER_SNAKE_CASE for global constants.
2. Component Best Practices
Prioritize functional components, TypeScript, and clear styling.
- Functional Components with Hooks: Always use functional components.
❌ BAD:
class MyComponent extends React.Component { /* ... */ }
✅ GOOD:const MyComponent: React.FC = () => { /* ... */ };
- TypeScript for Type Safety: All new code must be in TypeScript.
❌ BAD:
function greet(name) { return `Hello, ${name}`; }
✅ GOOD:const greet = (name: string): string => `Hello, ${name}`;
- Styling with
StyleSheet: Centralize styles for readability and performance.
❌ BAD:<Text style={{ fontSize: 16, color: 'blue' }}>Hello</Text>
✅ GOOD:import { StyleSheet, Text } from 'react-native';
const MyComponent = () => <Text style={styles.text}>Hello</Text>;
const styles = StyleSheet.create({ text: { fontSize: 16, color: 'blue' } });
3. Navigation with Expo Router
Leverage file-based routing for intuitive navigation.
4. Data & State Management
Manage state efficiently and immutably.
- Immutable State Updates: Always create new objects/arrays when updating state.
❌ BAD:
const [user, setUser] = useState({ name: 'Alice' });
user.name = 'Bob'; // Direct mutation
setUser(user);
✅ GOOD:const [user, setUser] = useState({ name: 'Alice' });
setUser(prev => ({ ...prev, name: 'Bob' }));
useEffect Dependency Arrays: Carefully manage dependencies to prevent unnecessary re-renders or infinite loops.
❌ BAD:useEffect(() => { fetchData(); }); // Runs on every render
✅ GOOD:useEffect(() => { fetchData(); }, []); // Runs once on mount
useEffect(() => { saveUser(user); }, [user]); // Runs when user changes
5. Environment Variables
Securely manage environment-specific values.
6. Performance Considerations
Optimize for a smooth user experience.
React.memo for Pure Components: Wrap pure functional components to prevent re-renders when props are unchanged.
❌ BAD:const MyItem = ({ data }) => { /* ... */ }; // Renders even if data is same object reference
✅ GOOD:const MyItem = React.memo(({ data }) => { /* ... */ });
useCallback and useMemo: Memoize functions and values passed to React.memo components or expensive computations.
❌ BAD:const handlePress = () => { /* ... */ }; // New function on every render
<MyItem />
✅ GOOD:const handlePress = useCallback(() => { /* ... */ }, []);
<MyItem />
- Lazy Loading Screens: For large apps, lazy load screens with
React.lazy and Suspense.const LazyScreen = React.lazy(() => import('./LazyScreen'));
// In your navigation or component:
<Suspense fallback={<LoadingSpinner />}>
<LazyScreen />
</Suspense>
7. Error Handling
Implement robust error handling mechanisms.
try/catch for Async Operations: Handle potential errors in asynchronous code.
❌ BAD:const fetchData = async () => { await api.get('/data'); };
✅ GOOD:const fetchData = async () => {
try {
await api.get('/data');
} catch (error) {
console.error('Failed to fetch data:', error);
// Display user-friendly error message
}
};
- Global Error Boundary: Catch UI errors in React components.
// src/components/ErrorBoundary.tsx
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
render() {
if (this.state.hasError) { return <Text>Something went wrong.</Text>; }
return this.props.children;
}
}
// In App.tsx or _layout.tsx
<ErrorBoundary><App /></ErrorBoundary>
8. Testing Approaches
Ensure code quality with comprehensive testing.
- Unit Tests with Jest & React Testing Library: Focus on component logic and user interactions. Aim for ≥80% coverage.
❌ BAD: No tests, or only shallow rendering tests.
✅ GOOD:
// src/components/Button.test.tsx
import { render, fireEvent } from '@testing-library/react-native';
import Button from './Button';
test('renders correctly and calls onPress', () => {
const mockOnPress = jest.fn();
const { getByText } = render(<Button title="Click Me" />);
fireEvent.press(getByText('Click Me'));
expect(mockOnPress).toHaveBeenCalledTimes(1);
});
- Snapshot Testing for UI Components: Capture UI structure to detect unintended changes.
// src/components/Button.test.tsx (continued)
import renderer from 'react-test-renderer';
test('renders correctly (snapshot)', () => {
const tree = renderer.create(<Button title="Test" => {}} />).toJSON();
expect(tree).toMatchSnapshot();
});
1---2name: expo3description: [Applies to: **/*] This guide provides definitive, opinionated best practices for building robust, performant, and maintainable cross-platform mobile applications with Expo, leveraging modern React Native patterns and the latest SDK features.4---56# expo Best Practices78This document outlines the definitive standards for Expo development. Adhere to these guidelines to ensure consistency, performance, and maintainability across our projects.910## 1. Code Organization and Structure1112Maintain a clean, scalable project structure.1314* **Root Structure:**15 * `assets/`: Static assets (images, fonts).16 * `src/`: All application logic.17 * `src/components/`: Reusable UI components.18 * `src/screens/`: Top-level components representing distinct app views.19 * `src/navigation/`: Navigation configuration (if not using Expo Router's `app/`).20 * `src/hooks/`: Custom React Hooks.21 * `src/utils/`: Utility functions, constants.22 * `src/services/`: API clients, data fetching logic.23* **Naming Conventions:**24 * **Files/Folders:** `kebab-case` for directories, `PascalCase` for components, `camelCase` for hooks/utils.25 * **Components:** `PascalCase` (e.g., `Button.tsx`, `HomeScreen.tsx`).26 * **Variables/Functions:** `camelCase` (e.g., `userName`, `fetchData`).27 * **Constants:** `UPPER_SNAKE_CASE` for global constants.2829## 2. Component Best Practices3031Prioritize functional components, TypeScript, and clear styling.3233* **Functional Components with Hooks:** Always use functional components.34 ❌ BAD:35 ```typescript36 class MyComponent extends React.Component { /* ... */ }37 ```38 ✅ GOOD:39 ```typescript40 const MyComponent: React.FC = () => { /* ... */ };41 ```42* **TypeScript for Type Safety:** All new code must be in TypeScript.43 ❌ BAD:44 ```javascript45 function greet(name) { return `Hello, ${name}`; }46 ```47 ✅ GOOD:48 ```typescript49 const greet = (name: string): string => `Hello, ${name}`;50 ```51* **Styling with `StyleSheet`:** Centralize styles for readability and performance.52 ❌ BAD:53 ```typescript54 <Text style={{ fontSize: 16, color: 'blue' }}>Hello</Text>55 ```56 ✅ GOOD:57 ```typescript58 import { StyleSheet, Text } from 'react-native';59 const MyComponent = () => <Text style={styles.text}>Hello</Text>;60 const styles = StyleSheet.create({ text: { fontSize: 16, color: 'blue' } });61 ```6263## 3. Navigation with Expo Router6465Leverage file-based routing for intuitive navigation.6667* **File-System Based Routing:** Use `app/` directory for routing.68 ❌ BAD: Manual stack/tab navigator setup in a single file for all routes.69 ✅ GOOD:70 ```71 // app/_layout.tsx72 import { Stack } from 'expo-router';73 export default function RootLayout() {74 return <Stack />;75 }7677 // app/index.tsx (maps to /)78 export default function HomePage() { /* ... */ }7980 // app/profile/[id].tsx (maps to /profile/:id)81 export default function ProfilePage() {82 const { id } = useLocalSearchParams();83 // ...84 }85 ```8687## 4. Data & State Management8889Manage state efficiently and immutably.9091* **Immutable State Updates:** Always create new objects/arrays when updating state.92 ❌ BAD:93 ```typescript94 const [user, setUser] = useState({ name: 'Alice' });95 user.name = 'Bob'; // Direct mutation96 setUser(user);97 ```98 ✅ GOOD:99 ```typescript100 const [user, setUser] = useState({ name: 'Alice' });101 setUser(prev => ({ ...prev, name: 'Bob' }));102 ```103* **`useEffect` Dependency Arrays:** Carefully manage dependencies to prevent unnecessary re-renders or infinite loops.104 ❌ BAD:105 ```typescript106 useEffect(() => { fetchData(); }); // Runs on every render107 ```108 ✅ GOOD:109 ```typescript110 useEffect(() => { fetchData(); }, []); // Runs once on mount111 useEffect(() => { saveUser(user); }, [user]); // Runs when user changes112 ```113114## 5. Environment Variables115116Securely manage environment-specific values.117118* **`EXPO_PUBLIC_` Prefix:** Use `.env` files with `EXPO_PUBLIC_` for client-side variables.119 ❌ BAD: `API_KEY=mysecret` in `.env` and `process.env.API_KEY`. (Exposed in bundle, not automatically injected)120 ✅ GOOD:121 ```122 // .env123 EXPO_PUBLIC_API_URL=https://api.example.com/staging124 ```125 ```typescript126 // In your code127 const apiUrl = process.env.EXPO_PUBLIC_API_URL;128 ```129 > **Warning:** Never store sensitive keys (e.g., private API keys) in `EXPO_PUBLIC_` variables. They are bundled client-side. Use EAS Secrets for server-side secrets.130131## 6. Performance Considerations132133Optimize for a smooth user experience.134135* **`React.memo` for Pure Components:** Wrap pure functional components to prevent re-renders when props are unchanged.136 ❌ BAD:137 ```typescript138 const MyItem = ({ data }) => { /* ... */ }; // Renders even if data is same object reference139 ```140 ✅ GOOD:141 ```typescript142 const MyItem = React.memo(({ data }) => { /* ... */ });143 ```144* **`useCallback` and `useMemo`:** Memoize functions and values passed to `React.memo` components or expensive computations.145 ❌ BAD:146 ```typescript147 const handlePress = () => { /* ... */ }; // New function on every render148 <MyItem onPress={handlePress} />149 ```150 ✅ GOOD:151 ```typescript152 const handlePress = useCallback(() => { /* ... */ }, []);153 <MyItem onPress={handlePress} />154 ```155* **Lazy Loading Screens:** For large apps, lazy load screens with `React.lazy` and `Suspense`.156 ```typescript157 const LazyScreen = React.lazy(() => import('./LazyScreen'));158 // In your navigation or component:159 <Suspense fallback={<LoadingSpinner />}>160 <LazyScreen />161 </Suspense>162 ```163164## 7. Error Handling165166Implement robust error handling mechanisms.167168* **`try/catch` for Async Operations:** Handle potential errors in asynchronous code.169 ❌ BAD:170 ```typescript171 const fetchData = async () => { await api.get('/data'); };172 ```173 ✅ GOOD:174 ```typescript175 const fetchData = async () => {176 try {177 await api.get('/data');178 } catch (error) {179 console.error('Failed to fetch data:', error);180 // Display user-friendly error message181 }182 };183 ```184* **Global Error Boundary:** Catch UI errors in React components.185 ```typescript186 // src/components/ErrorBoundary.tsx187 class ErrorBoundary extends React.Component {188 state = { hasError: false };189 static getDerivedStateFromError() { return { hasError: true }; }190 render() {191 if (this.state.hasError) { return <Text>Something went wrong.</Text>; }192 return this.props.children;193 }194 }195 // In App.tsx or _layout.tsx196 <ErrorBoundary><App /></ErrorBoundary>197 ```198199## 8. Testing Approaches200201Ensure code quality with comprehensive testing.202203* **Unit Tests with Jest & React Testing Library:** Focus on component logic and user interactions. Aim for ≥80% coverage.204 ❌ BAD: No tests, or only shallow rendering tests.205 ✅ GOOD:206 ```typescript207 // src/components/Button.test.tsx208 import { render, fireEvent } from '@testing-library/react-native';209 import Button from './Button';210211 test('renders correctly and calls onPress', () => {212 const mockOnPress = jest.fn();213 const { getByText } = render(<Button title="Click Me" onPress={mockOnPress} />);214 fireEvent.press(getByText('Click Me'));215 expect(mockOnPress).toHaveBeenCalledTimes(1);216 });217 ```218* **Snapshot Testing for UI Components:** Capture UI structure to detect unintended changes.219 ```typescript220 // src/components/Button.test.tsx (continued)221 import renderer from 'react-test-renderer';222223 test('renders correctly (snapshot)', () => {224 const tree = renderer.create(<Button title="Test" onPress={() => {}} />).toJSON();225 expect(tree).toMatchSnapshot();226 });227 ```