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."4source: "cursor_mdc"5---67# expo Best Practices89This document outlines the definitive standards for Expo development. Adhere to these guidelines to ensure consistency, performance, and maintainability across our projects.1011## 1. Code Organization and Structure1213Maintain a clean, scalable project structure.1415* **Root Structure:**16 * `assets/`: Static assets (images, fonts).17 * `src/`: All application logic.18 * `src/components/`: Reusable UI components.19 * `src/screens/`: Top-level components representing distinct app views.20 * `src/navigation/`: Navigation configuration (if not using Expo Router's `app/`).21 * `src/hooks/`: Custom React Hooks.22 * `src/utils/`: Utility functions, constants.23 * `src/services/`: API clients, data fetching logic.24* **Naming Conventions:**25 * **Files/Folders:** `kebab-case` for directories, `PascalCase` for components, `camelCase` for hooks/utils.26 * **Components:** `PascalCase` (e.g., `Button.tsx`, `HomeScreen.tsx`).27 * **Variables/Functions:** `camelCase` (e.g., `userName`, `fetchData`).28 * **Constants:** `UPPER_SNAKE_CASE` for global constants.2930## 2. Component Best Practices3132Prioritize functional components, TypeScript, and clear styling.3334* **Functional Components with Hooks:** Always use functional components.35 ❌ BAD:36 ```typescript37 class MyComponent extends React.Component { /* ... */ }38 ```39 ✅ GOOD:40 ```typescript41 const MyComponent: React.FC = () => { /* ... */ };42 ```43* **TypeScript for Type Safety:** All new code must be in TypeScript.44 ❌ BAD:45 ```javascript46 function greet(name) { return `Hello, ${name}`; }47 ```48 ✅ GOOD:49 ```typescript50 const greet = (name: string): string => `Hello, ${name}`;51 ```52* **Styling with `StyleSheet`:** Centralize styles for readability and performance.53 ❌ BAD:54 ```typescript55 <Text style={{ fontSize: 16, color: 'blue' }}>Hello</Text>56 ```57 ✅ GOOD:58 ```typescript59 import { StyleSheet, Text } from 'react-native';60 const MyComponent = () => <Text style={styles.text}>Hello</Text>;61 const styles = StyleSheet.create({ text: { fontSize: 16, color: 'blue' } });62 ```6364## 3. Navigation with Expo Router6566Leverage file-based routing for intuitive navigation.6768* **File-System Based Routing:** Use `app/` directory for routing.69 ❌ BAD: Manual stack/tab navigator setup in a single file for all routes.70 ✅ GOOD:71 ```72 // app/_layout.tsx73 import { Stack } from 'expo-router';74 export default function RootLayout() {75 return <Stack />;76 }7778 // app/index.tsx (maps to /)79 export default function HomePage() { /* ... */ }8081 // app/profile/[id].tsx (maps to /profile/:id)82 export default function ProfilePage() {83 const { id } = useLocalSearchParams();84 // ...85 }86 ```8788## 4. Data & State Management8990Manage state efficiently and immutably.9192* **Immutable State Updates:** Always create new objects/arrays when updating state.93 ❌ BAD:94 ```typescript95 const [user, setUser] = useState({ name: 'Alice' });96 user.name = 'Bob'; // Direct mutation97 setUser(user);98 ```99 ✅ GOOD:100 ```typescript101 const [user, setUser] = useState({ name: 'Alice' });102 setUser(prev => ({ ...prev, name: 'Bob' }));103 ```104* **`useEffect` Dependency Arrays:** Carefully manage dependencies to prevent unnecessary re-renders or infinite loops.105 ❌ BAD:106 ```typescript107 useEffect(() => { fetchData(); }); // Runs on every render108 ```109 ✅ GOOD:110 ```typescript111 useEffect(() => { fetchData(); }, []); // Runs once on mount112 useEffect(() => { saveUser(user); }, [user]); // Runs when user changes113 ```114115## 5. Environment Variables116117Securely manage environment-specific values.118119* **`EXPO_PUBLIC_` Prefix:** Use `.env` files with `EXPO_PUBLIC_` for client-side variables.120 ❌ BAD: `API_KEY=mysecret` in `.env` and `process.env.API_KEY`. (Exposed in bundle, not automatically injected)121 ✅ GOOD:122 ```123 // .env124 EXPO_PUBLIC_API_URL=https://api.example.com/staging125 ```126 ```typescript127 // In your code128 const apiUrl = process.env.EXPO_PUBLIC_API_URL;129 ```130 > **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.131132## 6. Performance Considerations133134Optimize for a smooth user experience.135136* **`React.memo` for Pure Components:** Wrap pure functional components to prevent re-renders when props are unchanged.137 ❌ BAD:138 ```typescript139 const MyItem = ({ data }) => { /* ... */ }; // Renders even if data is same object reference140 ```141 ✅ GOOD:142 ```typescript143 const MyItem = React.memo(({ data }) => { /* ... */ });144 ```145* **`useCallback` and `useMemo`:** Memoize functions and values passed to `React.memo` components or expensive computations.146 ❌ BAD:147 ```typescript148 const handlePress = () => { /* ... */ }; // New function on every render149 <MyItem onPress={handlePress} />150 ```151 ✅ GOOD:152 ```typescript153 const handlePress = useCallback(() => { /* ... */ }, []);154 <MyItem onPress={handlePress} />155 ```156* **Lazy Loading Screens:** For large apps, lazy load screens with `React.lazy` and `Suspense`.157 ```typescript158 const LazyScreen = React.lazy(() => import('./LazyScreen'));159 // In your navigation or component:160 <Suspense fallback={<LoadingSpinner />}>161 <LazyScreen />162 </Suspense>163 ```164165## 7. Error Handling166167Implement robust error handling mechanisms.168169* **`try/catch` for Async Operations:** Handle potential errors in asynchronous code.170 ❌ BAD:171 ```typescript172 const fetchData = async () => { await api.get('/data'); };173 ```174 ✅ GOOD:175 ```typescript176 const fetchData = async () => {177 try {178 await api.get('/data');179 } catch (error) {180 console.error('Failed to fetch data:', error);181 // Display user-friendly error message182 }183 };184 ```185* **Global Error Boundary:** Catch UI errors in React components.186 ```typescript187 // src/components/ErrorBoundary.tsx188 class ErrorBoundary extends React.Component {189 state = { hasError: false };190 static getDerivedStateFromError() { return { hasError: true }; }191 render() {192 if (this.state.hasError) { return <Text>Something went wrong.</Text>; }193 return this.props.children;194 }195 }196 // In App.tsx or _layout.tsx197 <ErrorBoundary><App /></ErrorBoundary>198 ```199200## 8. Testing Approaches201202Ensure code quality with comprehensive testing.203204* **Unit Tests with Jest & React Testing Library:** Focus on component logic and user interactions. Aim for ≥80% coverage.205 ❌ BAD: No tests, or only shallow rendering tests.206 ✅ GOOD:207 ```typescript208 // src/components/Button.test.tsx209 import { render, fireEvent } from '@testing-library/react-native';210 import Button from './Button';211212 test('renders correctly and calls onPress', () => {213 const mockOnPress = jest.fn();214 const { getByText } = render(<Button title="Click Me" onPress={mockOnPress} />);215 fireEvent.press(getByText('Click Me'));216 expect(mockOnPress).toHaveBeenCalledTimes(1);217 });218 ```219* **Snapshot Testing for UI Components:** Capture UI structure to detect unintended changes.220 ```typescript221 // src/components/Button.test.tsx (continued)222 import renderer from 'react-test-renderer';223224 test('renders correctly (snapshot)', () => {225 const tree = renderer.create(<Button title="Test" onPress={() => {}} />).toJSON();226 expect(tree).toMatchSnapshot();227 });228 ```