# API Integration Standards for React Native
This document outlines the coding standards and best practices for API integration in React Native applications. It aims to provide a comprehensive guide for developers to write maintainable, efficient, and secure code when interacting with backend services and external APIs. It covers modern approaches, design patterns, and the latest React Native features to ensure code quality and consistency.
## 1. Architectural Considerations
### 1.1. Decoupling API Logic
**Standard:** Separate API interaction concerns from UI components to promote reusability and testability.
* **Do This:** Use a dedicated service or repository layer for API calls.
* **Don't Do This:** Directly make API calls within React components.
**Why:** Decoupling makes components easier to test and reuse. It also simplifies debugging and allows for easier modifications to the API layer without affecting the UI.
"""javascript
// Good: API service layer
import axios from 'axios';
const API_BASE_URL = 'https://api.example.com';
const ApiService = {
get: async (endpoint, params = {}) => {
try {
const response = await axios.get("${API_BASE_URL}/${endpoint}", { params });
return response.data;
} catch (error) {
console.error('API GET error:', error);
throw error; // Re-throw the error for handling in the component
}
},
post: async (endpoint, data = {}) => {
try {
const response = await axios.post("${API_BASE_URL}/${endpoint}", data);
return response.data;
} catch (error) {
console.error('API POST error:', error);
throw error; // Re-throw the error for handling in the component
}
},
put: async (endpoint, data = {}) => {
try {
const response = await axios.put("${API_BASE_URL}/${endpoint}", data);
return response.data;
} catch (error) {
console.error('API PUT error:', error);
throw error; // Re-throw the error for handling in the component
}
},
delete: async (endpoint) => {
try {
const response = await axios.delete("${API_BASE_URL}/${endpoint}");
return response.data;
} catch (error) {
console.error('API DELETE error:', error);
throw error; // Re-throw the error for handling in the component
}
}
};
export default ApiService;
// Example Usage in a component
import React, { useState, useEffect } from 'react';
import { View, Text, ActivityIndicator } from 'react-native';
import ApiService from './ApiService';
const MyComponent = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const result = await ApiService.get('data');
setData(result);
setLoading(false);
} catch (err) {
setError(err.message || 'An error occurred');
setLoading(false);
}
};
fetchData();
}, []);
if (loading) {
return ;
}
if (error) {
return Error: {error};
}
return (
Data: {data ? JSON.stringify(data) : 'No data'}
);
};
export default MyComponent;
"""
### 1.2. Centralized Error Handling
**Standard:** Implement a centralized error handling mechanism to manage API errors consistently.
* **Do This:** Create custom error classes and a global error handler.
* **Don't Do This:** Handle errors directly within individual API calls in components without a unified approach.
**Why:** Centralized error handling provides a consistent user experience and simplifies debugging.
"""javascript
// Custom error class
class ApiError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'ApiError';
this.statusCode = statusCode;
}
}
// API service with error handling
const ApiService = {
get: async (endpoint, params = {}) => {
try {
const response = await axios.get("${API_BASE_URL}/${endpoint}", { params });
if (response.status >= 400) {
throw new ApiError("Request failed with status ${response.status}", response.status);
}
return response.data;
} catch (error) {
console.error('API GET error:', error);
if (error instanceof ApiError) {
// Handle known API errors
throw error;
} else {
// Wrap unexpected errors
throw new ApiError('An unexpected error occurred', 500);
}
}
}
};
// Global error handler (example)
const handleApiError = (error) => {
if (error instanceof ApiError) {
// Log the error, display a user-friendly message, or take other actions
console.error('API Error:', error.message, error.statusCode);
// Example: Display an alert to the user
Alert.alert('API Error', error.message);
} else {
// Handle other types of errors (e.g., network errors)
console.error('Unexpected Error:', error);
Alert.alert('Error', 'An unexpected error occurred. Please try again later.');
}
};
// Usage in a component
import React, { useState, useEffect } from 'react';
import { View, Text, ActivityIndicator, Alert } from 'react-native';
import ApiService from './ApiService';
const MyComponent = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const result = await ApiService.get('data');
setData(result);
setLoading(false);
} catch (err) {
setError(err);
handleApiError(err); // Use the global error handler
setLoading(false);
}
};
fetchData();
}, []);
if (loading) {
return ;
}
if (error) {
return Error: {error.message || 'An error occurred'};
}
return (
Data: {data ? JSON.stringify(data) : 'No data'}
);
};
export default MyComponent;
"""
### 1.3. Data Transformation
**Standard:** Transform API responses into a format suitable for the application's UI and data models.
* **Do This:** Use a transformation function or layer between the API service and the UI.
* **Don't Do This:** Directly use API responses in UI without validation or transformation.
**Why:** Data transformation isolates the UI from API changes and ensures data consistency across the application.
"""javascript
// API service
const ApiService = {
getUsers: async () => {
const response = await axios.get("${API_BASE_URL}/users");
return response.data;
}
};
// Data transformation function
const transformUser = (user) => ({
id: user.id,
fullName: "${user.firstName} ${user.lastName}",
email: user.email,
});
// Usage in component
import React, { useState, useEffect } from 'react';
import { View, Text } from 'react-native';
import ApiService from './ApiService';
const UserList = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
const fetchUsers = async () => {
const apiUsers = await ApiService.getUsers();
const transformedUsers = apiUsers.map(transformUser);
setUsers(transformedUsers);
};
fetchUsers();
}, []);
return (
{users.map((user) => (
{user.fullName} - {user.email}
))}
);
};
export default UserList;
"""
## 2. HTTP Client Selection and Configuration
### 2.1. Choosing the Right HTTP Client
**Standard:** Use "axios" or the built-in "fetch" API for making HTTP requests.
* **Do This:** Evaluate "axios" and "fetch" based on project requirements (e.g., interceptors, cancellation). "axios" is generally preferred for its ease of use and features, but "fetch" is a lightweight alternative if those are not needed.
* **Don't Do This:** Rely on deprecated or less-maintained HTTP clients.
**Why:** Modern HTTP clients offer better features, performance, and security compared to older libraries. "axios" provides automatic JSON transformation, request cancellation, and interceptors.
### 2.2. Configuring Timeout and Retries
**Standard:** Set appropriate timeouts and implement retry mechanisms to handle network issues.
* **Do This:** Configure timeouts to prevent indefinite waiting and use retry strategies for transient errors.
* **Don't Do This:** Use default timeout settings or ignore network errors.
"""javascript
// Axios with timeout and retry
import axios from 'axios';
const API_BASE_URL = 'https://api.example.com';
const axiosInstance = axios.create({
baseURL: API_BASE_URL,
timeout: 5000, // 5 seconds timeout
});
axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.code === 'ECONNABORTED' && !originalRequest._retry) {
originalRequest._retry = true;
console.log('Retrying request...');
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second before retrying
return axiosInstance(originalRequest);
}
return Promise.reject(error);
}
);
const ApiService = {
get: async (endpoint, params = {}) => {
try {
const response = await axiosInstance.get(endpoint, { params });
return response.data;
} catch (error) {
console.error('API GET error:', error);
throw error; // Re-throw the error for handling in the component
}
}
};
export default ApiService;
"""
### 2.3. Handling Request Cancellation
**Standard:** Implement request cancellation when the component unmounts or the user initiates a new action.
* **Do This:** Use "AbortController" with "fetch" or cancellation tokens with "axios".
* **Don't Do This:** Leave requests running in the background, which can lead to memory leaks and unexpected behavior.
"""javascript
// Fetch with AbortController
import React, { useState, useEffect } from 'react';
import { View, Text } from 'react-native';
const MyComponent = () => {
const [data, setData] = useState(null);
useEffect(() => {
const abortController = new AbortController();
const signal = abortController.signal;
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data', { signal });
const result = await response.json();
setData(result);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Fetch error:', error);
}
}
};
fetchData();
return () => {
abortController.abort(); // Cancel the request on unmount
};
}, []);
return (
Data: {data ? JSON.stringify(data) : 'Loading...'}
);
};
export default MyComponent;
"""
## 3. Authentication and Authorization
### 3.1. Securely Storing Authentication Tokens
**Standard:** Store authentication tokens securely using "react-native-keychain" or "AsyncStorage".
* **Do This:** Use Keychain for sensitive data and "AsyncStorage" for less sensitive data. Encrypt tokens before storing them.
* **Don't Do This:** Store tokens in plain text in memory or local storage.
**Why:** Secure storage prevents unauthorized access to authentication tokens. "react-native-keychain" provides native-level security for iOS and Android.
"""javascript
// Secure storage using react-native-keychain
import * as Keychain from 'react-native-keychain';
const storeCredentials = async (username, password) => {
try {
await Keychain.setGenericPassword(username, password);
console.log('Credentials stored securely');
} catch (error) {
console.error('Keychain storage error:', error);
}
};
const retrieveCredentials = async () => {
try {
const credentials = await Keychain.getGenericPassword();
if (credentials) {
console.log('Credentials retrieved successfully');
return credentials;
} else {
console.log('No credentials stored');
return null;
}
} catch (error) {
console.error('Keychain retrieval error:', error);
return null;
}
};
const deleteCredentials = async () => {
try {
await Keychain.resetGenericPassword();
console.log('Credentials deleted successfully');
} catch (error) {
console.error('Keychain deletion error:', error);
}
};
// Usage example (login)
const login = async (username, password) => {
try {
const response = await ApiService.post('login', { username, password });
const { token } = response;
await storeCredentials(username, token); // Store the token securely
// Further actions after successful login
} catch (error) {
console.error('Login failed:', error);
}
};
// Usage example (retrieve token)
const getToken = async () => {
const credentials = await retrieveCredentials();
if (credentials) {
return credentials.password; // Return the token
}
return null;
};
"""
### 3.2. Implementing Refresh Tokens
**Standard:** Use refresh tokens to obtain new access tokens without requiring the user to re-authenticate.
* **Do This:** Implement a refresh token flow that handles token expiration gracefully.
* **Don't Do This:** Rely solely on long-lived access tokens.
**Why:** Refresh tokens enhance security and user experience by minimizing the need for frequent re-authentication.
"""javascript
// Axios interceptor for token refresh
axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = await retrieveRefreshToken();
if (!refreshToken) {
// Redirect to login if no refresh token is available
return Promise.reject(error);
}
const refreshResponse = await axios.post('/refresh', { refreshToken });
const { newAccessToken, newRefreshToken } = refreshResponse.data;
await storeCredentials('username', newAccessToken);
await storeRefreshToken(newRefreshToken);
axiosInstance.defaults.headers.common['Authorization'] = "Bearer ${newAccessToken}";
originalRequest.headers['Authorization'] = "Bearer ${newAccessToken}";
return axiosInstance(originalRequest);
} catch (refreshError) {
// Redirect to login if refresh fails
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
const storeRefreshToken = async (refreshToken) => {
try {
await AsyncStorage.setItem('refreshToken', refreshToken);
} catch (error) {
console.error('Error storing refresh token:', error);
}
};
const retrieveRefreshToken = async () => {
try {
const refreshToken = await AsyncStorage.getItem('refreshToken');
return refreshToken;
} catch (error) {
console.error('Error retrieving refresh token:', error);
return null;
}
};
"""
### 3.3. Securing API Keys
**Standard:** Protect API keys from exposure by using environment variables and secure storage.
* **Do This:** Store API keys in ".env" files during development and use native secrets management in production.
* **Don't Do This:** Hardcode API keys directly in the source code.
**Why:** API keys are sensitive and should be protected to prevent abuse and unauthorized access.
"""javascript
// Using environment variables
import { API_KEY } from '@env';
const ApiService = {
getData: async () => {
const response = await axios.get("${API_BASE_URL}/data?apiKey=${API_KEY}");
return response.data;
}
};
"""
## 4. Data Handling and State Management
### 4.1. Optimistic Updates
**Standard:** Implement optimistic updates for UI responsiveness.
* **Do This:** Update the UI immediately after a request is initiated and revert the update if the request fails.
* **Don't Do This:** Wait for the server response before updating the UI.
**Why:** Optimistic updates provide a smoother user experience by making the UI feel more responsive.
"""javascript
import React, { useState } from 'react';
import { View, Text, Button } from 'react-native';
import ApiService from './ApiService';
const LikeButton = ({ postId, initialLikes }) => {
const [likes, setLikes] = useState(initialLikes);
const [isLiked, setIsLiked] = useState(false);
const handleLike = async () => {
const previousLikes = likes;
const newLikes = isLiked ? likes - 1 : likes + 1;
// Optimistic update
setLikes(newLikes);
setIsLiked(!isLiked);
try {
await ApiService.post("posts/${postId}/like", { liked: !isLiked });
} catch (error) {
// Revert update on error
setLikes(previousLikes);
setIsLiked(isLiked);
console.error('Failed to update likes:', error);
}
};
return (
Likes: {likes}
);
};
export default LikeButton;
"""
### 4.2. Pagination
**Standard:** Implement pagination for displaying large datasets.
* **Do This:** Use server-side pagination to retrieve data in chunks and implement infinite scrolling or load more buttons.
* **Don't Do This:** Load entire datasets at once, which can cause performance issues.
"""javascript
import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, Button, ActivityIndicator } from 'react-native';
import ApiService from './ApiService';
const ItemList = () => {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const fetchItems = async () => {
if (loading || !hasMore) return;
setLoading(true);
try {
const response = await ApiService.get("items?page=${page}&limit=10");
if (response.length === 0) {
setHasMore(false);
} else {
setItems((prevItems) => [...prevItems, ...response]);
setPage((prevPage) => prevPage + 1);
}
} catch (error) {
console.error('Failed to fetch items:', error);
setHasMore(false);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchItems();
}, []);
const renderItem = ({ item }) => (
{item.name}
);
const loadMore = () => {
fetchItems();
};
const renderFooter = () => (
loading ? (
) : hasMore ? (
) : null
);
return (
item.id.toString()}
ListFooterComponent={renderFooter}
onEndReached={loadMore}
onEndReachedThreshold={0.5}
/>
);
};
export default ItemList;
"""
### 4.3. Using State Management Libraries
**Standard:** Use state management libraries like Redux, Zustand, or Context API for complex state management.
* **Do This:** Choose a state management solution based on project complexity and team familiarity.
* **Don't Do This:** Use local component state for data that needs to be shared across multiple components.
**Why:** State management libraries simplify data sharing and ensure data consistency across the application.
"""javascript
// Redux example
// actions.js
export const FETCH_DATA_REQUEST = 'FETCH_DATA_REQUEST';
export const FETCH_DATA_SUCCESS = 'FETCH_DATA_SUCCESS';
export const FETCH_DATA_FAILURE = 'FETCH_DATA_FAILURE';
export const fetchDataRequest = () => ({ type: FETCH_DATA_REQUEST });
export const fetchDataSuccess = (data) => ({ type: FETCH_DATA_SUCCESS, payload: data });
export const fetchDataFailure = (error) => ({ type: FETCH_DATA_FAILURE, payload: error });
export const fetchData = () => {
return async (dispatch) => {
dispatch(fetchDataRequest());
try {
const response = await ApiService.get('data');
dispatch(fetchDataSuccess(response));
} catch (error) {
dispatch(fetchDataFailure(error));
}
};
};
// reducer.js
const initialState = {
data: null,
loading: false,
error: null,
};
const dataReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_DATA_REQUEST:
return { ...state, loading: true, error: null };
case FETCH_DATA_SUCCESS:
return { ...state, loading: false, data: action.payload };
case FETCH_DATA_FAILURE:
return { ...state, loading: false, error: action.payload };
default:
return state;
}
};
export default dataReducer;
// component.js
import React, { useEffect } from 'react';
import { View, Text, ActivityIndicator } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { fetchData } from './actions';
const MyComponent = () => {
const dispatch = useDispatch();
const data = useSelector((state) => state.data.data);
const loading = useSelector((state) => state.data.loading);
const error = useSelector((state) => state.data.error);
useEffect(() => {
dispatch(fetchData());
}, [dispatch]);
if (loading) {
return ;
}
if (error) {
return Error: {error.message || 'An error occurred'};
}
return (
Data: {data ? JSON.stringify(data) : 'No data'}
);
};
export default MyComponent;
"""
## 5. Performance Optimization
### 5.1. Caching API Responses
**Standard:** Cache API responses to reduce network requests and improve performance.
* **Do This:** Use in-memory caching or persistent storage (e.g., "AsyncStorage") to store API responses. Utilize libraries like "react-native-cache" or implement custom caching logic.
* **Don't Do This:** Cache sensitive data without proper encryption or ignore cache invalidation.
**Why:** Caching reduces latency and bandwidth usage, resulting in a faster and more responsive application.
"""javascript
// Caching with AsyncStorage
import AsyncStorage from '@react-native-async-storage/async-storage';
const CACHE_KEY = 'api_data';
const CACHE_EXPIRY = 60 * 60 * 1000; // 1 hour
const ApiService = {
getData: async () => {
const cachedData = await AsyncStorage.getItem(CACHE_KEY);
const cacheExpiry = await AsyncStorage.getItem("${CACHE_KEY}_expiry");
if (cachedData && cacheExpiry && Date.now() < parseInt(cacheExpiry)) {
console.log('Using cached data');
return JSON.parse(cachedData);
}
const response = await axios.get("${API_BASE_URL}/data");
const data = response.data;
await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(data));
await AsyncStorage.setItem("${CACHE_KEY}_expiry", Date.now() + CACHE_EXPIRY);
return data;
}
};
"""
### 5.2. Using Memoization
**Standard:** Use memoization techniques to prevent unnecessary API calls.
* **Do This:** Use "React.memo" for functional components and "shouldComponentUpdate" for class components to memoize API results based on props.
* **Don't Do This:** Make redundant API calls when the data hasn't changed.
**Why:** Memoization optimizes rendering performance by preventing re-renders when the input props haven't changed.
"""javascript
import React from 'react';
import { View, Text } from 'react-native';
const MyComponent = React.memo(({ data }) => {
console.log('Rendering MyComponent');
return (
Data: {JSON.stringify(data)}
);
});
export default MyComponent;
"""
### 5.3. Batching API Requests
**Standard:** Batch multiple API requests into a single request to reduce network overhead.
* **Do This:** Use techniques like GraphQL or custom batching endpoints to combine multiple requests.
* **Don't Do This:** Make numerous small API calls when the data can be retrieved in a single request.
**Why:** Batching reduces the number of HTTP requests, improving performance, especially on slow or unreliable networks.
## 6. Security Best Practices
### 6.1. Input Validation
**Standard:** Validate all user inputs before sending them to the API.
* **Do This:** Use validation libraries (e.g., "yup", "joi") to validate input data and sanitize user input to prevent injection attacks.
* **Don't Do This:** Trust user input without validation.
**Why:** Input validation prevents malicious data from being sent to the server, protecting against security vulnerabilities.
"""javascript
import * as yup from 'yup';
const schema = yup.object().shape({
username: yup.string().required().min(3).max(20),
password: yup.string().required().min(8),
email: yup.string().email().required(),
});
const validateInput = async (data) => {
try {
await schema.validate(data, { abortEarly: false });
return { valid: true, errors: null };
} catch (error) {
const errors = {};
error.inner.forEach(err => {
errors[err.path] = err.message;
});
return { valid: false, errors };
}
};
// Example Usage
const handleSubmit = async (data) => {
const { valid, errors } = await validateInput(data);
if (valid) {
// Submit data to API
try {
await ApiService.post('register', data);
} catch (error) {
// Handle API error
}
} else {
// Display validation errors
console.log(errors);
}
};
"""
### 6.2. HTTPS
**Standard:** Use HTTPS for all API requests to encrypt data in transit.
* **Do This:** Ensure that all API endpoints use HTTPS. Configure your HTTP client to enforce HTTPS.
* **Don't Do This:** Use HTTP for sensitive data transmission.
**Why:** HTTPS protects data from eavesdropping and tampering during transmission.
### 6.3. Rate Limiting
**Standard:** Implement rate limiting on the server-side to prevent abuse and denial-of-service attacks.
* **Do This:** Monitor API usage and implement rate limiting to protect against abuse.
* **Don't Do This:** Allow unlimited API requests from a single client.
**Why:** Rate limiting prevents malicious users from overwhelming the server with excessive requests. This is enforced server-side, but understanding this interaction is important for client-side development.
## 7. Testing
### 7.1. Unit Testing
**Standard:** Write unit tests for API service functions.
* **Do This:** Use testing frameworks (e.g., Jest) to test API service functions in isolation. Mock API dependencies to ensure test stability.
* **Don't Do This:** Skip unit testing for API integration logic.
**Why:** Unit tests verify that API service functions work correctly and help prevent regressions.
"""javascript
// ApiService.test.js
import ApiService from './ApiService';
import axios from 'axios';
jest.mock('axios');
describe('ApiService', () => {
it('should fetch data successfully', async () => {
const mockData = { message: 'Success' };
axios.get.mockResolvedValue({ data: mockData });
const data = await ApiService.getData();
expect(data).toEqual(mockData);
expect(axios.get).toHaveBeenCalledWith("${API_BASE_URL}/data");
});
it('should handle errors when fetching data', async () => {
const mockError = new Error('Request failed');
axios.get.mockRejectedValue(mockError);
await expect(ApiService.getData()).rejects.toThrow(mockError);
expect(axios.get).toHaveBeenCalledWith("${API_BASE_URL}/data");
});
});
"""
### 7.2. Integration Testing
**Standard:** Write integration tests to verify the interaction between the UI and the API.
* **Do This:** Use testing frameworks (e.g., Detox, Appium) to test end-to-end scenarios involving API calls.
* **Don't Do This:** Skip integration testing, which can lead to undetected issues in the API integration.
**Why:** Integration tests verify that the UI and API work together correctly, ensuring a seamless user experience.
## 8. Monitoring and Logging
### 8.1. Logging API Requests and Responses
**Standard:** Log API requests and responses for debugging and monitoring purposes.
* **Do This:** Use logging libraries (e.g., "react-native-logs") to log API requests, responses, and errors. Be mindful of privacy considerations and avoid logging sensitive data.
* **Don't Do This:** Ignore logging, which makes it difficult to diagnose and resolve issues.
**Why:** Logging provides valuable insights into API usage and helps identify and resolve issues quickly.
### 8.2. Monitoring API Performance
**Standard:** Monitor API performance metrics to identify bottlenecks and optimize performance.
* **Do This:** Use monitoring tools (e.g., New Relic, Datadog) to track API response times, error rates, and resource usage.
* **Don't Do This:** Ignore API performance, which can lead to slow and unresponsive applications.
**Why:** Monitoring helps identify and resolve performance issues proactively, ensuring a smooth user experience.
By adhering to these coding standards and best practices, React Native developers can create robust, efficient, and secure applications that integrate seamlessly with backend services and external APIs. This comprehensive guide serves as a valuable resource for professional development teams aiming to achieve high-quality code and maintainable applications. Remember to stay updated with the latest React Native features and adapt these standards as needed to suit your specific project requirements.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Security Best Practices Standards for React Native This document outlines security best practices for React Native development. Following these standards will help protect your applications against common vulnerabilities, ensuring the confidentiality, integrity, and availability of user data. This document assumes you already have a basic familiarity with React Native development. ## 1. Data Storage ### 1.1 Secure Local Storage **Why:** Storing sensitive information locally without proper encryption can expose it to unauthorized access. **Do This:** * Use secure storage libraries like "react-native-keychain" for storing sensitive data (passwords, tokens, API keys). This encrypts data at rest. * If "react-native-keychain" doesn't meet your needs for non-sensitive data, use "react-native-encrypted-storage", which offers a higher-level API than raw "AsyncStorage". **Don't Do This:** * Avoid storing sensitive data in plain text using "AsyncStorage". **Code Example:** """javascript import * as Keychain from 'react-native-keychain'; const storeCredentials = async (username, password) => { try { await Keychain.setGenericPassword(username, password); console.log('Credentials stored securely!'); } catch (error) { console.error('Keychain storage error:', error); } }; const retrieveCredentials = async () => { try { const credentials = await Keychain.getGenericPassword(); if (credentials) { console.log('Credentials retrieved successfully'); return { username: credentials.username, password: credentials.password, }; } else { console.log('No credentials stored'); return null; } } catch (error) { console.error('Keychain retrieval error:', error); return null; } }; // Usage Example const storeUserCredentials = async () => { await storeCredentials('myUsername', 'mySecretPassword'); }; const getUserCredentials = async () => { const credentials = await retrieveCredentials(); if (credentials) { console.log('Username:', credentials.username); console.log('Password:', credentials.password); } }; """ **Anti-Pattern:** Directly storing API keys in source code or configuration files. This makes them easily accessible if the code is compromised. **Great Code:** Utilizing environment variables and build-time configuration to manage API keys, combined with encrypted storage at runtime. """javascript // .env file (never commit this to your repository) API_KEY=your_secret_api_key """ """javascript // app.json { "expo": { "name": "MyApp", "slug": "myapp", "version": "1.0.0", "extra": { "apiKey": process.env.API_KEY } } } """ """javascript import Constants from 'expo-constants'; const getApiKey = () => { if (Constants.expoConfig && Constants.expoConfig.extra) { return Constants.expoConfig.extra.apiKey; } return null; }; const apiKey = getApiKey(); """ ### 1.2 Data Masking **Why:** Prevents direct exposure of sensitive data in logs, analytics, or error reports. **Do This:** * Mask sensitive data before logging or transmitting it. Libraries such as "crypto-js" can provide robust hashing. * Avoid logging sensitive information directly (e.g., passwords, credit card numbers). **Don't Do This:** * Log full user details or personally identifiable information (PII) without proper anonymization or redaction. **Code Example:** """javascript import CryptoJS from 'crypto-js'; const logEvent = (eventData) => { const maskedEventData = { ...eventData, creditCardNumber: eventData.creditCardNumber ? CryptoJS.SHA256(eventData.creditCardNumber).toString() : null, }; console.log('Event Data:', maskedEventData); // Send maskedEventData to analytics service }; // Usage Example const event = { type: 'payment', amount: 100, creditCardNumber: '1234567890123456', }; logEvent(event); """ ## 2. Network Communication ### 2.1 HTTPS Enforcement **Why:** Ensures data transmission is encrypted and prevents man-in-the-middle attacks. **Do This:** * Use HTTPS for all API requests. Verify SSL certificates to prevent SSL pinning bypasses. * Ensure your server-side infrastructure is configured to enforce HTTPS. **Don't Do This:** * Make API requests over HTTP, especially for sensitive data. **Code Example:** """javascript const fetchData = async () => { try { const response = await fetch('https://api.example.com/data', { // Note the HTTPS method: 'GET', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { throw new Error("HTTP error! Status: ${response.status}"); } const data = await response.json(); console.log('Data:', data); } catch (error) { console.error('Fetch error:', error); } }; """ **Anti-pattern:** Hardcoding "http" in your API URL. **Great Code:** Using environment variables to define the API base URL which has "https". """javascript const API_BASE_URL = process.env.API_BASE_URL; // Should be an https URL const fetchData = async () => { try { const response = await fetch("${API_BASE_URL}/data", { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { throw new Error("HTTP error! Status: ${response.status}"); } const data = await response.json(); console.log('Data:', data); } catch (error) { console.error('Fetch error:', error); } }; """ ### 2.2 SSL Pinning **Why:** Validates the server's SSL certificate to prevent man-in-the-middle attacks by only trusting a specific certificate or public key. **Do This:** * Implement SSL pinning using libraries like "react-native-ssl-pinning". **Don't Do This:** * Rely solely on the operating system's trusted certificate authorities; they can be compromised. **Code Example:** """javascript import { check } from 'react-native-ssl-pinning'; const fetchDataWithSSLPinning = async () => { try { const response = await check('api.example.com', { // replace with your API endpoint publicKey: 'YOUR_PUBLIC_KEY_HERE', // replace with your public key }); if (response.valid) { console.log('SSL Pinning Validation Successful!'); // Proceed with fetching data } else { console.error('SSL Pinning Validation Failed!'); // Handle the error (e.g., refuse connection) } } catch (error) { console.error('SSL Pinning Check Error:', error); // Handle the error } }; """ ### 2.3 Input Validation **Why:** Prevents injection attacks and ensures data integrity. **Do This:** * Validate all user inputs on both the client and server sides. Sanitize inputs to prevent injection attacks. * Use appropriate data types and formats for input fields. * Utilize validation libraries like "Yup" or "React Hook Form" with validation schemas. **Don't Do This:** * Trust client-side validation alone. Always validate on the server. * Directly use user input in database queries or commands without sanitization. **Code Example:** """javascript import * as Yup from 'yup'; import { useForm, Controller } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { TextInput, Button, View, Text } from 'react-native'; const validationSchema = Yup.object().shape({ email: Yup.string().email('Invalid email').required('Email is required'), password: Yup.string() .min(8, 'Password must be at least 8 characters') .required('Password is required'), }); const LoginForm = () => { const { control, handleSubmit, formState: { errors } } = useForm({ resolver: yupResolver(validationSchema) }); const onSubmit = data => { console.log(data); // Submit validated data }; return ( <View> <Controller control={control} render={({ field: { onChange, onBlur, value } }) => ( <TextInput style={{ borderWidth: 1, padding: 10, marginBottom: 10 }} onBlur={onBlur} onChangeText={onChange} value={value} placeholder="Email" keyboardType="email-address" /> )} name="email" defaultValue="" /> {errors.email && <Text style={{ color: 'red' }}>{errors.email.message}</Text>} <Controller control={control} render={({ field: { onChange, onBlur, value } }) => ( <TextInput style={{ borderWidth: 1, padding: 10, marginBottom: 10 }} onBlur={onBlur} onChangeText={onChange} value={value} placeholder="Password" secureTextEntry={true} /> )} name="password" defaultValue="" /> {errors.password && <Text style={{ color: 'red' }}>{errors.password.message}</Text>} <Button title="Login" onPress={handleSubmit(onSubmit)} /> </View> ); }; export default LoginForm; """ ## 3. Authentication and Authorization ### 3.1 Secure Authentication Flows **Why:** Robust authentication is the foundation of a secure application. **Do This:** * Implement multi-factor authentication (MFA) where possible. * Use established authentication protocols like OAuth 2.0 or OpenID Connect. * Store passwords securely using bcrypt or Argon2 hashing algorithms on the server-side. * Use biometric authentication (Face ID, Touch ID) via "expo-local-authentication" for enhanced security. **Don't Do This:** * Store passwords in plain text or use weak hashing algorithms like MD5 or SHA1. * Implement custom authentication protocols without proper security review. **Code Example (Biometric Authentication):** """javascript import * as LocalAuthentication from 'expo-local-authentication'; import { useState, useEffect } from 'react'; import { View, Text, Button, Alert } from 'react-native'; const BiometricAuth = () => { const [isBiometricSupported, setIsBiometricSupported] = useState(false); useEffect(() => { (async () => { const compatible = await LocalAuthentication.hasHardwareAsync(); setIsBiometricSupported(compatible); })(); }); const fallBackToDefaultAuth = () => { Alert.alert( 'Fallback Authentication', 'Please enter your password:', [ {text: 'Cancel', style: 'cancel'}, {text: 'OK', onPress: () => console.log('Password authentication handled here')}, ], {cancelable: false} ); }; const authenticate = async () => { const result = await LocalAuthentication.authenticateAsync({ promptMessage: 'Authenticate to access app', fallbackLabel: 'Enter Password', cancelLabel: 'Cancel', }); if(result.success) { Alert.alert('Authentication successful!'); } else { fallBackToDefaultAuth() console.log('Authentication failed', result.error); } }; return ( <View> <Text> {isBiometricSupported ? 'Your device is compatible with biometric authentication' : 'Face or Fingerprint scanner not available on this device'} </Text> <Button title="Authenticate" onPress={authenticate} /> </View> ); }; export default BiometricAuth; """ ### 3.2 Authorization **Why:** Prevents unauthorized access to resources. **Do This:** * Implement role-based access control (RBAC) to restrict access based on user roles. * Use JSON Web Tokens (JWT) for secure authorization. Verify JWT signatures on the server-side. * Implement proper access control checks in your API endpoints. **Don't Do This:** * Rely solely on client-side authorization checks. * Store sensitive data in JWTs; use them only for authorization. **Code Example (JWT Verification - Example Node.js Server):** """javascript // Node.js server example const jwt = require('jsonwebtoken'); const verifyToken = (req, res, next) => { const token = req.headers['authorization']; if (!token) { return res.status(403).send({ message: 'No token provided!' }); } jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => { if (err) { return res.status(401).send({ message: 'Unauthorized!' }); } req.userId = decoded.id; next(); }); }; //Example usage (protecting an API endpoint) app.get('/api/profile',verifyToken, (req, res) => { //accessing data after succesfull authentication res.status(200).send({message: "Access Granted"}) }) """" ## 4. Dependencies and Updates ### 4.1 Dependency Management **Why:** Vulnerabilities in third-party libraries can compromise your application. **Do This:** * Regularly update dependencies to patch security vulnerabilities. * Use a dependency scanning tool to identify known vulnerabilities (e.g., "npm audit", "yarn audit"). * Lock dependencies using "package-lock.json" or "yarn.lock" to ensure consistent versions across environments. **Don't Do This:** * Use outdated or unmaintained libraries. * Ignore dependency security warnings. **Code Example (Dependency Update Workflow):** 1. Run "npm audit" or "yarn audit" 2. Review identified vulnerabilities. 3. Run "npm update" or "yarn upgrade" to update vulnerable packages. 4. Test your application thoroughly after updating dependencies. ### 4.2 React Native Upgrades **Why:** Newer versions of React Native often include important security patches and performance improvements. Failing to upgrade leaves your application vulnerable to known exploits and missing out on optimizations. **Do This:** * Keep React Native and related packages updated to at least the stable *minor* version releases. Major version upgrades should be evaluated on a seperate time frame. * Review the release notes for each update to identify security-related fixes and breaking changes. * Use "react-native upgrade" to update your React Native project. * Test your application thoroughly after each upgrade. * Pay close attention to the deprecation warnings, and address them according to the most up-to-date documentation **Don't Do This:** * Stay on outdated React Native versions. * Ignore the deprecation warnings. ## 5. Code Obfuscation (Defense in Depth) ### **5.1 Code Obfuscation** **Why:** Makes it harder for attackers to reverse engineer your app, understand its logic, and identify vulnerabilities **Do This:** * Use tools like ProGuard (Android) and JavaScript obfuscators (e.g., "javascript-obfuscator") as part of your build process * Consider commercial React Native build services to automate the obfuscation process **Don't Do This:** * Realize that it's not a silver bullet, but a deterrent. * Rely *solely* on obfuscation as your only security measure **Code Example (JavaScript Obfuscation using "javascript-obfuscator"):** 1. Install "javascript-obfuscator": """bash npm install javascript-obfuscator --save-dev """ 2. Create an obfuscation script (e.g., "obfuscate.js"): """javascript const JavaScriptObfuscator = require('javascript-obfuscator'); const fs = require('fs'); const fileToObfuscate = 'src/App.js'; // Replace with your main file const outputFile = 'dist/App.obfuscated.js'; fs.readFile(fileToObfuscate, 'utf8', (err, data) => { if (err) { console.error('Error reading file:', err); return; } const obfuscationResult = JavaScriptObfuscator.obfuscate(data, { compact: true, controlFlowFlattening: true, deadCodeInjection: true, // ... other options }); fs.writeFile(outputFile, obfuscationResult.getObfuscatedCode(), (err) => { if (err) { console.error('Error writing file:', err); } else { console.log('File obfuscated successfully!'); } }); }); """ 3. Add a script to your "package.json": """json "scripts": { "obfuscate": "node obfuscate.js" } """ 4. Run the obfuscation script: """bash npm run obfuscate """ 5. Update your build process to use the obfuscated file. ## 6. Sensitive Information Handling ### 6.1 In-Memory Handling **Why:** Minimizing the time sensitive data exists in memory reduces the attack surface **Do This:** * Clear sensitive data from memory as soon as it's no longer needed. * Avoid storing sensitive information in global variables. * Use local variables with limited scope for processing sensitive data. **Don't Do This:** * Leave sensitive data lingering in memory longer than necessary. * Rely on garbage collection to automatically clear sensitive data (it's not guaranteed). **Code Example:** """javascript const processSensitiveData = (data) => { let sensitiveInfo = data.creditCardNumber; //Do processing. sensitiveInfo = null; // Clear from memory console.warn("sensitiveInfo is NUll NOW.", sensitiveInfo) }; """ ## 7. Error Handling and Logging ### 7.1 Secure Error Handling **Why:** Prevents exposing sensitive information in error messages. **Do This:** * Implement custom error handling to prevent exposing sensitive data in stack traces or error messages. * Log errors to a secure location, not to the console in production. **Don't Do This:** * Display detailed error messages to end-users in production. * Log sensitive data in error logs. **Code Example:** """javascript const fetchData = async () => { try { const response = await fetch('https://api.example.com/data'); // Replace with your actual endpoint if (!response.ok) { throw new Error("HTTP error! Status: ${response.status}"); } const data = await response.json(); console.log('Data:', data); } catch (error) { // Log error to a secure logging service (e.g., Sentry, Firebase Crashlytics) console.error('Fetch error occurred. Refer to secure logs for details.'); // Display a generic error message to the user // Alert.alert('Error', 'An unexpected error occurred. Please try again later.'); } }; """ ## 8. Platform-Specific Considerations ### 8.1 iOS Security **Why:** iOS has specific security features that should be utilized. **Do This:** * Utilize the iOS Keychain for secure storage of credentials. * Implement App Transport Security (ATS) to enforce HTTPS connections. * Protect against jailbreaking by detecting and responding appropriately. ### 8.2 Android Security **Why:** Understand Android-specific attack vectors and mitigation strategies. **Do This:** * Utilize Android Keystore for secure storage of encryption keys and credentials. * Protect against reverse engineering and tampering using ProGuard. * Implement runtime application self-protection (RASP) techniques. ## 9. Regular Security Audits **Why:**Proactively identify and address vulnerabilities. **Do This:** * Conduct regular code reviews and security audits. * Use static analysis tools to identify potential security flaws. * Perform penetration testing to simulate real-world attacks. * Keep abreast of the latest security threats and vulnerabilities. ## Conclusion Maintaining a secure React Native application is an ongoing process. By following these best practices, you can significantly reduce the risk of security vulnerabilities and protect your users' data. Regular reviews, updates, and vigilance are key to ensuring a secure and reliable application.
# Performance Optimization Standards for React Native This document outlines the performance optimization standards for React Native applications. Following these guidelines will help you create fast, responsive, and resource-efficient apps. These standards are designed to leverage the features of the latest React Native versions. ## 1. Architecture and Component Design ### 1.1. Component Granularity * **Do This:** Break down complex screens into smaller, reusable, and independent components. * **Don't Do This:** Create monolithic components that handle too much logic and rendering. **Why:** Smaller components are easier to optimize, test, and maintain. They also enable more efficient re-rendering. **Example:** """jsx // Good: Separate components for different parts of a screen const Header = () => { return <Text>My App</Text>; }; const ProductList = ({ products }) => { return ( <FlatList data={products} renderItem={({ item }) => <ProductItem product={item} />} keyExtractor={item => item.id} /> ); }; const ProductItem = ({ product }) => { return <Text>{product.name}</Text> } const MainScreen = ({ products }) => { return ( <View> <Header /> <ProductList products={products} /> </View> ); }; // Bad: One giant component const MainScreenUnoptimized = ({ products }) => { return ( <View> <Text>My App</Text> <FlatList data={products} renderItem={({ item }) => <Text>{item.name}</Text>} keyExtractor={item => item.id} /> </View> ); }; """ ### 1.2. Pure Components and "React.memo" * **Do This:** Use "React.PureComponent" or "React.memo" for components that render the same output given the same props and context. * **Don't Do This:** Rely on default "React.Component" for all components, leading to unnecessary re-renders. **Why:** "PureComponent" performs a shallow prop and state comparison before rendering. "React.memo" provides similar functionality for functional components. This avoids wasteful re-renders. **Example:** """jsx import React from 'react'; // Using React.memo for a functional component const MyComponent = React.memo(({ data }) => { console.log('MyComponent rendered'); return <Text>{data.value}</Text>; }); // Alternatively, use React.PureComponent for class components class MyPureComponent extends React.PureComponent { render() { console.log('MyPureComponent rendered'); return <Text>{this.props.data.value}</Text>; } } // Usage Example, assuming data prop rarely changes: const App = () => { const [count, setCount] = React.useState(0); const data = React.useMemo(() => ({ value: "Data ${count}" }), [count < 5]); // memoize data for first few counts return ( <View> <MyComponent data={data} /> <MyPureComponent data={data} /> <Button title="Increment" onPress={() => setCount(c => c + 1)} /> </View> ); }; """ ### 1.3. Immutability * **Do This:** Treat state as immutable. Use methods like "...spread" or "Object.assign()" to create new objects/arrays when updating state. * **Don't Do This:** Directly modify state objects. **Why:** Immutable data structures allow React to easily detect changes and trigger re-renders efficiently because it can do a simple reference equality check. **Example:** """jsx // Good const updateStateImmutably = () => { setState(prevState => ({ ...prevState, value: 'new value', })); }; // Also good (using Object.assign): const updateStateImmutablyAlternative = () => { setState(prevState => Object.assign({}, prevState, { value: 'new value' })); }; // Bad const updateStateMutably = () => { state.value = 'new value'; // DON'T DO THIS! setState(state); // This will not reliably trigger a re-render. }; """ ### 1.4 Virtualized Lists (FlatList, SectionList) * **Do This:** Use "FlatList" or "SectionList" for rendering large lists of data. Provide "keyExtractor", "getItemLayout" (when applicable), and "initialNumToRender" to optimize performance. * **Don't Do This:** Use "ScrollView" with a "map" operation, especially for long lists. **Why:** "FlatList" and "SectionList" render only the visible items, improving initial render time and memory usage. **Example:** """jsx import React from 'react'; import { FlatList, Text, View, StyleSheet } from 'react-native'; const data = Array.from({ length: 1000 }, (_, i) => ({ id: i.toString(), text: "Item ${i}" })); const renderItem = ({ item }) => ( <View style={styles.item}> <Text>{item.text}</Text> </View> ); const App = () => { return ( <FlatList data={data} renderItem={renderItem} keyExtractor={item => item.id} initialNumToRender={10} // Render this many items initially getItemLayout={(data, index) => ({length: 50, offset: 50 * index, index})} // if item height is fixed. crucial for long lists! removeClippedSubviews={true} // Optimization flag for large lists /> ); }; const styles = StyleSheet.create({ item: { padding: 20, borderBottomWidth: 1, borderBottomColor: '#eee', }, }); """ ### 1.5 Lazy Loading * **Do This:** Load data or resources (e.g., images) only when they are needed. * **Don't Do This:** Load all data at once, especially on app startup. **Why:** Reduces initial load time and memory consumption. **Example (using "react-native-fast-image" for image optimization and lazy loading):** """jsx import FastImage from 'react-native-fast-image'; const LazyImage = ({ source }) => { const [loaded, setLoaded] = React.useState(false); return ( <FastImage style={{ width: 200, height: 200 }} source={source} resizeMode={FastImage.resizeMode.contain} onLoad={() => setLoaded(true)} //Show placeholder until the image is loaded completely {...(loaded ? {} : {fallback: true})} /> ); }; // Usage: <LazyImage source={{ uri: 'https://example.com/image.jpg' }} /> """ ### 1.6 Debouncing and Throttling * **Do This:** Use debouncing (e.g., for search input) and throttling (e.g., for scroll events) to limit the frequency of function calls. * **Don't Do This:** Execute computationally expensive functions directly on every event. **Why:** Prevents excessive computations and UI updates, improving responsiveness. **Example (using "lodash" for debouncing):** """jsx import React, { useState, useCallback } from 'react'; import { TextInput } from 'react-native'; import { debounce } from 'lodash'; const SearchInput = () => { const [searchTerm, setSearchTerm] = useState(''); const handleSearch = (text) => { console.log('Searching for:', text); // Perform your search logic here setSearchTerm(text); // Keep state up to date }; const debouncedSearch = useCallback( debounce(handleSearch, 300), // Wait 300ms before executing handleSearch [] ); const onChangeText = (text) => { debouncedSearch(text); }; return ( <TextInput placeholder="Search..." onChangeText={onChangeText} /> ); }; """ ## 2. JavaScript and Bridge Performance ### 2.1. Minimize Bridge Crossings * **Do This:** Batch multiple related operations into a single bridge call when possible. Use "useNativeDriver: true" for animations. * **Don't Do This:** Make excessive small calls over the bridge as it is an expensive operation **Why:** Reduces overhead and improves performance. The React Native bridge is the conduit for communication between JavaScript and native code. **Example (using "Animated" with "useNativeDriver"):** """jsx import React, { useRef, useEffect } from 'react'; import { View, Animated, Button } from 'react-native'; const AnimatedView = () => { const fadeAnim = useRef(new Animated.Value(0)).current; // Initial value for opacity: 0 const fadeIn = () => { Animated.timing(fadeAnim, { toValue: 1, duration: 5000, useNativeDriver: true, // Enable native driver for smoother animations }).start(); }; return ( <View> <Animated.View style={{ ...props.style, opacity: fadeAnim, }} > <Text>Fading View</Text> </Animated.View> <Button title="Fade In" onPress={fadeIn} /> </View> ); }; """ ### 2.2. Optimize JavaScript Code * **Do This:** Use efficient algorithms and data structures. Avoid unnecessary computations in render functions. * **Don't Do This:** Perform complex calculations or data transformations directly within the "render" function. **Why:** JavaScript performance directly affects UI responsiveness. **Example:** """jsx import React from 'react'; const EfficientComponent = ({ data }) => { // Memoize the result to avoid recomputation on every render if data doesn't change const processedData = React.useMemo(() => { console.log("Processing data"); return data.map(item => item * 2); //Example Computation }, [data]); return ( <View> {processedData.map(item => <Text key={item}>{item}</Text>)} </View> ); }; """ ### 2.3. Avoid Console.log in Production * **Do This:** Remove or disable "console.log" statements in production builds. * **Don't Do This:** Leave "console.log" statements in production code. **Why:** "console.log" can significantly impact performance, especially on older devices. ### 2.4 Reduce Bundle Size * **Do This:** Use code splitting, tree shaking, and minification to reduce the bundle size and improve initial load time * **Don't Do This:** Include unused code or large libraries in your bundle without optimization **Why:** Smaller bundle sizes lead to faster download and startup times **Example (using dynamic imports for code splitting):** """jsx import React, { useState, useEffect } from 'react'; import { Button, View, Text } from 'react-native'; const App = () => { const [module, setModule] = useState(null); useEffect(() => { // Dynamically import a module when needed const loadModule = async () => { const loadedModule = await import('./MyHeavyComponent'); setModule(() => loadedModule.default); }; loadModule(); }, []); return ( <View> {module ? <Module /> : <Text>Loading...</Text>} </View> ); }; """ ### 2.5 Hermes Engine * **Do This:** Use the Hermes JavaScript engine, especially for Android builds. * **Don't Do This:** Stick to the default JavaScriptCore engine without considering Hermes. **Why:** Hermes is optimized for React Native and provides faster app startup times, reduced memory usage, and smaller app sizes. **How:** Enable Hermes in your "android/app/build.gradle" file: """gradle project.ext.react = [ entryFile: "index.js" ] def enableHermes = project.ext.react.get("enableHermes", true); if (enableHermes) { def hermesPath = "../../node_modules/hermes-engine/android/"; debugImplementation files(hermesPath + "hermes-debug.aar") releaseImplementation files(hermesPath + "hermes-release.aar") } else { debugImplementation jscFlavor == null ? 'org.webkit:android-jsc:+' : jscFlavor releaseImplementation jscFlavor == null ? 'org.webkit:android-jsc:+' : jscFlavor } """ ## 3. Image Optimization ### 3.1. Image Format and Size * **Do This:** Use optimized image formats like WebP (if supported) or JPEG. Resize images to the required display size. * **Don't Do This:** Use large, unoptimized PNG files. **Why:** Reduces app size and memory usage. ### 3.2. Caching * **Do This:** Cache images to avoid repeated downloads. Use libraries like "react-native-fast-image" or implement custom caching mechanisms. * **Don't Do This:** Download the same image multiple times. **Why:** Improves loading speed and reduces network bandwidth. **Example (using "react-native-fast-image"):** """jsx import FastImage from 'react-native-fast-image'; const MyImage = ({ source }) => { return ( <FastImage style={{ width: 200, height: 200 }} source={source} resizeMode={FastImage.resizeMode.contain} /> ); }; """ ### 3.3. Image Resizing * **Do This:** Resize images server-side or during the build process. * **Don't Do This:** Rely on React Native to resize large images at runtime . **Why:** Offloads processing from the device and improves loading performance. ## 4. Memory Management ### 4.1. Avoid Memory Leaks * **Do This:** Deregister event listeners and timers when components unmount. Use "useEffect" with a cleanup function. * **Don't Do This:** Leave dangling event listeners or timers. **Why:** Prevents memory leaks and improves app stability. **Example:** """jsx import React, { useEffect } from 'react'; import { DeviceEventEmitter } from 'react-native'; const MyComponent = () => { useEffect(() => { const subscription = DeviceEventEmitter.addListener('myEvent', () => { console.log('Event received'); }); return () => { // Clean up the subscription when the component unmounts subscription.remove(); }; }, []); return <Text>My Component</Text>; }; """ ### 4.2. Reduce Redundant State * **Do This:** Avoid storing the same information in multiple state variables particularly if one can be derived from the other. **Why**: Prevents unnecessary renders and keeps the apps state management straightforward. **Example:** """jsx import React, {useState} from 'react'; const MyComponent = () => { const [items, setItems] = useState([]); // deriving "count" prevents the need for a useState const count = items.length; const doSomething = () => { /* ... */ }; return( <View> <Text>Length {count}</Text> </View> ); }; """ ## 5. Native Modules Optimization ### 5.1 Efficient Native Module Design * **Do This:** Design native module methods to perform complex operations efficiently and return results in a structured format. Minimize the number of calls between JavaScript and native code. Utilize background threads/queues on the native side to avoid blocking the main thread. * **Don't Do This:** Perform simple operations in native modules that could be done in JavaScript. Execute long-running or blocking operations on the main thread. **Why:** Optimizes native execution and prevents ANRs (Application Not Responding) errors on Android, or frozen UI on iOS. **Example (Android - using AsyncTask):** """java // Android Java import android.os.AsyncTask; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; public class MyNativeModule extends ReactContextBaseJavaModule { public MyNativeModule(ReactApplicationContext reactContext) { super(reactContext); } @Override public String getName() { return "MyNativeModule"; } @ReactMethod public void doLongRunningTask(String input, Callback callback) { new AsyncTask<String, Void, String>() { @Override protected String doInBackground(String... params) { // Simulate a long-running task try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } String result = "Processed: " + params[0]; return result; } @Override protected void onPostExecute(String result) { callback.invoke(null, result); // Pass result back to JavaScript } }.execute(input); } } """ """javascript // JavaScript import { NativeModules } from 'react-native'; const doLongRunningTask = async (input) => { try { const result = await new Promise((resolve, reject) => { NativeModules.MyNativeModule.doLongRunningTask(input, (error, data) => { if (error) { reject(error); } else { resolve(data); } }); }); console.log('Result:', result); } catch (error) { console.error('Error:', error); } }; """ ### 5.2 Data Serialization * **Do This:** Utilize efficient data serialization techniques (e.g., JSON) when passing data between JavaScript and native modules * **Don't Do This:** Pass large, complex data structures without appropriate serialization, impacting bridge performance. **Why:** Reduces the overhead of data transfer across the React Native bridge thus improving communication performance ## 6. Monitoring and Profiling ### 6.1. Performance Monitoring * **Do This:** Use tools like Flipper or React Native Performance Monitor to identify performance bottlenecks. * **Don't Do This:** Deploy without insight into performance metrics **Why:** Provides awareness of performance regressions and potential areas for improvement. ### 6.2. Profiling Tools * **Do This:** Use the React Profiler or native platform profiling tools (e.g., Android Studio Profiler, Xcode Instruments) to analyze component render times and identify performance issues. * **Don't Do This:** Make optimizations without profiling **Why:** Helps pinpoint the root cause of performance problems. The React Profiler helps you identify slow components and understand why components are re-rendering. ## 7. Updates and Deprecations * **Do This:** Stay updated with the latest React Native releases. * **Don't Do This:** Use deprecated features. **Why:** New releases bring performance improvements, bug fixes, and new features. Deprecated features may be removed in future versions. These performance standards offer a solid foundation for creating optimized React Native applications. Be sure to continuously monitor, profile, and adapt to the evolving React Native ecosystem.
# Core Architecture Standards for React Native This document outlines the core architectural standards for React Native applications. It provides guidelines for structuring projects, organizing code, and applying architectural patterns to ensure maintainability, scalability, and performance. These standards are based on the latest React Native features and best practices. ## 1. Project Structure and Organization ### 1.1. Standard Adopt a feature-based or domain-driven structure for your React Native projects. * **Do This:** Group related components, hooks, utilities, and styles within a dedicated directory representing a specific feature or domain area. * **Don't Do This:** Organize files based on their type (e.g., "components/", "screens/", "utils/") at the top level, as this leads to scattered, less maintainable code. **Why:** Feature-based organization improves code discoverability, simplifies refactoring, and minimizes dependencies between unrelated parts of the application. It also supports team-based development by allowing developers to focus on specific areas of the codebase. **Example:** """ src/ ├── components/ # Shared, reusable components ├── features/ │ ├── authentication/ │ │ ├── screens/ │ │ │ ├── LoginScreen.tsx │ │ │ └── SignupScreen.tsx │ │ ├── components/ │ │ │ ├── AuthForm.tsx │ │ │ └── PasswordInput.tsx │ │ ├── hooks/ │ │ │ └── useAuth.ts │ │ ├── services/ │ │ │ └── authService.ts │ │ └── styles/ │ │ └── authStyles.ts │ ├── home/ │ │ ├── screens/ │ │ │ └── HomeScreen.tsx │ │ ├── components/ │ │ │ └── WelcomeMessage.tsx │ │ └── ... ├── navigation/ │ └── AppNavigator.tsx ├── services/ # Global services for APIs │ └── api.ts ├── types/ # Shared Typescript types │ └── index.ts └── App.tsx """ ### 1.2. Standard Use a clear naming convention for files and directories. * **Do This:** Use PascalCase for component names (e.g., "LoginScreen.tsx"), camelCase for variables and functions (e.g., "handleLogin", "apiClient"), and descriptive names for directories (e.g., "authentication", "home"). * **Don't Do This:** Use cryptic abbreviations or inconsistent naming styles. **Why:** Consistent naming improves code readability and makes it easier to understand the purpose of each file and directory. **Example:** """ src/ └── features/ └── profile/ ├── screens/ │ └── ProfileScreen.tsx // Correct: PascalCase for components ├── components/ │ └── UserAvatar.tsx // Correct: PascalCase for components ├── hooks/ │ └── useProfileData.ts // Correct: camelCase for hooks ├── utils/ │ └── profileUtils.ts // Correct: camelCase for utilities """ ### 1.3. Standard Employ a modular architecture with well-defined boundaries. * **Do This:** Divide the application into independent modules responsible for specific functionalities (e.g., authentication, data fetching, UI rendering). * **Don't Do This:** Create monolithic components that handle multiple responsibilities or allow tight coupling between unrelated modules. **Why:** Modular architecture improves code reusability, simplifies testing, and allows for easier maintenance and scaling of the application. **Example:** """typescript // authentication/services/authService.ts import api from '../../services/api'; //Import the API from Global Services const login = async (credentials: LoginCredentials): Promise<User> => { const response = await api.post('/login', credentials); return response.data; }; const signup = async (userData: SignupData): Promise<User> => { const response = await api.post('/signup', userData); return response.data; } export default { login, signup }; // authentication/screens/LoginScreen.tsx import React, { useState } from 'react'; import authService from '../services/authService'; const LoginScreen = () => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const handleLogin = async () => { try { const user = await authService.login({ username, password }); // Handle successful login console.log('Logged in:', user); } catch (error) { // Handle login error console.error('Login failed:', error); } }; return ( // ... UI elements for login form ... <Button title="Login" onPress={handleLogin} /> ); }; export default LoginScreen; """ **Anti-pattern:** Avoid directly importing components or UI elements from other features directly into a single component. For instance, having navigation logic embedded deeply inside a component makes it tightly coupled and hard to test. Instead, navigation should be handled by services or navigation components specifically to maintain modular structure. ## 2. Architectural Patterns ### 2.1. Standard Favor the use of React Context API or state management libraries like Redux or Zustand for managing global application state. * **Do This:** Use Context API for simple state management scenarios or opt for a state management library for complex applications with centralized data needs. * **Don't Do This:** Pass data through multiple levels of components using props (prop drilling) or rely on mutable global variables for storing application state. **Why:** Centralized state management improves data flow predictability, simplifies debugging, and allows for easier sharing of data between components. **Example (Context API):** """typescript // src/context/AuthContext.tsx import React, { createContext, useState, useContext, ReactNode } from 'react'; interface AuthContextType { user: User | null; login: (userData: User) => void; logout: () => void; } interface AuthProviderProps { children: ReactNode; } const AuthContext = createContext<AuthContextType | undefined>(undefined); export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => { const [user, setUser] = useState<User | null>(null); const login = (userData: User) => { setUser(userData); }; const logout = () => { setUser(null); }; const value: AuthContextType = { user, login, logout, }; return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; }; export const useAuth = () => { // this is proper syntax for the latest react native const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used within an AuthProvider'); } return context; }; // src/features/profile/screens/ProfileScreen.tsx import React from 'react'; import { useAuth } from '../../context/AuthContext'; const ProfileScreen = () => { const { user, logout } = useAuth(); if (!user) { return <Text>Please login.</Text>; } return ( <> <Text>Welcome, {user.username}!</Text> <Button title="Logout" onPress={logout} /> </> ); }; export default ProfileScreen; """ **Example (Redux Toolkit):** """typescript // src/store/slices/authSlice.ts import { createSlice, PayloadAction } from '@reduxjs/toolkit'; interface AuthState { user: User | null; isLoading: boolean; error: string | null; } const initialState: AuthState = { user: null, isLoading: false, error: null, }; const authSlice = createSlice({ name: 'auth', initialState, reducers: { loginStart(state) { state.isLoading = true; }, loginSuccess(state, action: PayloadAction<User>) { state.user = action.payload; state.isLoading = false; state.error = null; }, loginFailure(state, action: PayloadAction<string>) { state.user = null; state.isLoading = false; state.error = action.payload; }, logout(state) { state.user = null; state.isLoading = false; state.error = null; } }, }); export const { loginStart, loginSuccess, loginFailure, logout } = authSlice.actions; export default authSlice.reducer; // src/store/store.ts import { configureStore } from '@reduxjs/toolkit'; import authReducer from './slices/authSlice'; const store = configureStore({ reducer: { auth: authReducer, }, }); export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch; export default store; // src/features/profile/screens/ProfileScreen.tsx import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { RootState, AppDispatch } from '../../store/store'; import { logout } from '../../store/slices/authSlice'; const ProfileScreen = () => { const user = useSelector((state: RootState) => state.auth.user); const dispatch = useDispatch<AppDispatch>(); const handleLogout = () => { dispatch(logout()); }; if (!user) { return <Text>Please login.</Text>; } return ( <> <Text>Welcome, {user.username}!</Text> <Button title="Logout" onPress={handleLogout} /> </> ); }; export default ProfileScreen; """ ### 2.2. Standard Implement the "Separation of Concerns" principle to keep the logic of components, styling, and data handling separate. * **Do This:** Use functional components and React Hooks to separate UI rendering logic from state management and side effects. Move styling to separate stylesheet files. * **Don't Do This:** Put business logic, API calls, or complex calculations directly within components' render methods. Avoid inline styling as much as possible. **Why:** Separation of concerns makes components easier to understand, test, and reuse. It also improves code maintainability by allowing you to modify UI logic, styling or data handling without affecting other parts of the component. **Example:** """typescript // src/features/home/components/WelcomeMessage.tsx import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; interface WelcomeMessageProps { name: string; } const WelcomeMessage: React.FC<WelcomeMessageProps> = ({ name }) => { return ( <View style={styles.container}> <Text style={styles.text}>Welcome, {name}!</Text> </View> ); }; const styles = StyleSheet.create({ container: { padding: 16, backgroundColor: '#f0f0f0', borderRadius: 8, }, text: { fontSize: 18, fontWeight: 'bold', }, }); export default WelcomeMessage; """ ### 2.3. Standard Use the "Adapter Pattern" for communication with external libraries or APIs. * **Do This:** Create adapter classes or functions that translate data between the format expected by your application and the format provided by the external system. * **Don't Do This:** Directly use external libraries or APIs within your components without any abstraction. **Why:** The Adapter Pattern isolates your application from changes in external systems and allows you to easily switch between different implementations. It also makes testing easier by allowing you to mock the adapter and simulate different scenarios. **Example:** """typescript // src/services/apiAdapters/userAdapter.ts import { User } from '../../types'; interface ApiUser { id: number; firstName: string; lastName: string; emailAddress: string; } const adaptApiUserToUser = (apiUser: ApiUser): User => { return { id: apiUser.id, fullName: "${apiUser.firstName} ${apiUser.lastName}", email: apiUser.emailAddress, }; }; export default adaptApiUserToUser; // src/services/api.ts import axios from 'axios'; import adaptApiUserToUser from './apiAdapters/userAdapter'; const apiClient = axios.create({ baseURL: 'https://api.example.com', }); const getUser = async (id: number): Promise<User> => { const response = await apiClient.get("/users/${id}"); return adaptApiUserToUser(response.data); }; export default { getUser }; // src/features/profile/screens/ProfileScreen.tsx import React, { useState, useEffect } from 'react'; import api from '../../services/api'; const ProfileScreen = () => { const [user, setUser] = useState<User | null>(null); useEffect(() => { const fetchUser = async () => { const userData = await api.getUser(123); // Fetch user with ID 123 setUser(userData); }; fetchUser(); }, []); if (!user) { return <Text>Loading...</Text>; } return ( <> <Text>Full Name: {user.fullName}</Text> <Text>Email: {user.email}</Text> </> ); }; export default ProfileScreen; """ ## 3. Component Design ### 3.1. Standard Build reusable and composable components. * **Do This:** Design components that can be used in multiple parts of the application and easily customized through props. * **Don't Do This:** Create highly specific components that are only used in one place or have complex internal logic that is difficult to modify. **Why:** Reusable components reduce code duplication, improve consistency, and simplify maintenance. **Example:** """typescript // src/components/Button.tsx import React from 'react'; import { TouchableOpacity, Text, StyleSheet } from 'react-native'; interface ButtonProps { title: string; onPress: () => void; color?: string; // Optional prop for button color } const Button: React.FC<ButtonProps> = ({ title, onPress, color = '#007BFF' }) => { return ( <TouchableOpacity style={[styles.button, { backgroundColor: color }]} onPress={onPress} > <Text style={styles.text}>{title}</Text> </TouchableOpacity> ); }; const styles = StyleSheet.create({ button: { padding: 12, borderRadius: 8, alignItems: 'center', justifyContent: 'center', }, text: { color: 'white', fontSize: 16, fontWeight: 'bold', }, }); export default Button; // Usage in different screens: // src/features/authentication/screens/LoginScreen.tsx <Button title="Login" onPress={handleLogin} /> // src/features/home/screens/HomeScreen.tsx <Button title="Go to Settings" onPress={navigateToSettings} color="#28A745" /> """ ### 3.2. Standard Use functional components with React Hooks for state management and side effects. * **Do This:** Prefer functional components and hooks over class components, especially for new code. * **Don't Do This:** Use class components unless you are working with legacy code or require specific lifecycle methods. **Why:** Functional components with hooks are more concise, easier to test, and promote code reuse through custom hooks. They also align with the recommended approach in modern React development. **Example:** """typescript // src/hooks/useCounter.ts import { useState } from 'react'; const useCounter = (initialValue: number = 0) => { const [count, setCount] = useState(initialValue); const increment = () => { setCount(count + 1); }; const decrement = () => { setCount(count - 1); }; return { count, increment, decrement }; }; export default useCounter; // src/components/Counter.tsx import React from 'react'; import useCounter from '../hooks/useCounter'; const Counter = () => { const { count, increment, decrement } = useCounter(10); return ( <> <Text>Count: {count}</Text> <Button title="Increment" onPress={increment} /> <Button title="Decrement" onPress={decrement} /> </> ); }; export default Counter; """ ### 3.3. Standard Use PropTypes or TypeScript for type checking and validating component props. * **Do This:** Define the expected types of component props using PropTypes or TypeScript interfaces/types. * **Don't Do This:** Skip type checking altogether, as this can lead to runtime errors and unexpected behavior. **Why:** Type checking helps catch errors early in the development process, improves code reliability, and makes components easier to understand and use. TypeScript is increasingly preferred for React Native projects. **Example (TypeScript):** """typescript // src/components/Greeting.tsx import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; interface GreetingProps { name: string; age?: number; // Optional prop } const Greeting: React.FC<GreetingProps> = ({ name, age }) => { return ( <View style={styles.container}> <Text style={styles.text}>Hello, {name}!</Text> {age && <Text style={styles.text}>You are {age} years old.</Text>} </View> ); }; const styles = StyleSheet.create({ container: { padding: 16, }, text: { fontSize: 16, }, }); export default Greeting; """ ## 4. Data Handling ### 4.1. Standard Abstract API calls into dedicated service modules. * **Do This:** Create service modules that encapsulate the logic for interacting with APIs, including data fetching, transformation, and error handling. * **Don't Do This:** Directly make API calls within components, as this leads to tightly coupled code and makes it difficult to test and maintain. **Why:** Service modules promote code reuse, improve testability, and allow you to easily switch between different data sources or API implementations. **Example:** """typescript // src/services/api.ts import axios from 'axios'; const apiClient = axios.create({ baseURL: 'https://api.example.com', }); const getUsers = async (): Promise<User[]> => { const response = await apiClient.get('/users'); return response.data; }; const createUser = async (userData: User): Promise<User> => { const response = await apiClient.post('/users', userData); return response.data; }; export default { getUsers, createUser }; // src/features/userList/screens/UserListScreen.tsx import React, { useState, useEffect } from 'react'; import api from '../../services/api'; const UserListScreen = () => { const [users, setUsers] = useState<User[]>([]); useEffect(() => { const fetchUsers = async () => { const userList = await api.getUsers(); setUsers(userList); }; fetchUsers(); }, []); return ( // ... Render the list of users ... ); }; export default UserListScreen; """ ### 4.2. Standard Implement proper error handling for API calls and data processing. * **Do This:** Use try-catch blocks to handle potential errors during API calls and data processing. Display user-friendly error messages to the user. * **Don't Do This:** Ignore errors or display technical error messages to the user. **Why:** Proper error handling improves the user experience, prevents unexpected crashes, and makes the application more robust. **Example:** """typescript // src/services/api.ts import axios from 'axios'; const getUsers = async (): Promise<User[]> => { try { const response = await axios.get('/users'); return response.data; } catch (error: any) { // type assertion: tells Typescript that error is of type any console.error('Error fetching users:', error.message); throw new Error('Failed to fetch users'); // Re-throw the error for handling in the component } }; export default { getUsers }; // src/features/userList/screens/UserListScreen.tsx import React, { useState, useEffect } from 'react'; import api from '../../services/api'; import { Alert } from 'react-native'; const UserListScreen = () => { const [users, setUsers] = useState<User[]>([]); const [error, setError] = useState<string | null>(null); useEffect(() => { const fetchUsers = async () => { try { const userList = await api.getUsers(); setUsers(userList); setError(null); // Clear any previous error } catch (err: any) { setError(err.message || 'Failed to fetch users'); // type assertion: tells Typescript that err is of type any Alert.alert('Error', 'Failed to load users. Please try again later.'); //Enhanced error message to the user } }; fetchUsers(); }, []); if (error) { return <Text>Error: {error}</Text>; } return ( // ... Render the list of users ... ); }; export default UserListScreen; """ ### 4.3. Standard Use data transformation techniques to adapt data to the specific needs of your application. * **Do This:** Transform data received from APIs or other sources into a consistent and convenient format for use in your components. * **Don't Do This:** Directly use raw data from external sources without any transformation. **Why:** Data transformation improves code clarity, reduces dependencies on external data formats, and allows you to easily adapt to changes in data sources. Refer to the "Adapter Pattern" (2.3) for examples of data transformation. ## 5. Navigation ### 5.1. Standard Use React Navigation for handling application navigation. * **Do This:** Use React Navigation's pre-built navigators and components for creating different navigation patterns (e.g., stack, tab, drawer). * **Don't Do This:** Implement custom navigation logic from scratch, as this is complex and error-prone. **Why:** React Navigation provides a powerful and flexible API for creating complex navigation flows in React Native applications. **Example:** """typescript // src/navigation/AppNavigator.tsx import React from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import HomeScreen from '../features/home/screens/HomeScreen'; import ProfileScreen from '../features/profile/screens/ProfileScreen'; const Stack = createStackNavigator(); const AppNavigator = () => { return ( <NavigationContainer> <Stack.Navigator initialRouteName="Home"> <Stack.Screen name="Home" component={HomeScreen} /> <Stack.Screen name="Profile" component={ProfileScreen} /> </Stack.Navigator> </NavigationContainer> ); }; export default AppNavigator; // src/features/home/screens/HomeScreen.tsx import React from 'react'; import { Button, View, Text } from 'react-native'; const HomeScreen = ({ navigation }: { navigation: any }) => { return ( <View> <Text>Home Screen</Text> <Button title="Go to Profile" onPress={() => navigation.navigate('Profile')} /> </View> ); }; export default HomeScreen; """ ### 5.2. Standard Define navigation parameters using TypeScript interfaces for type safety. * **Do This:** Create interfaces that specify the expected types of navigation parameters for each screen. * **Don't Do This:** Pass navigation parameters as plain JavaScript objects without any type checking. **Why:** Type-safe navigation parameters help catch errors early in the development process and improve code reliability. **Example:** """typescript // src/navigation/types.ts export type RootStackParamList = { Home: undefined; Profile: { userId: string }; Settings: undefined; }; // src/screens/ProfileScreen.tsx import React from 'react'; import { RouteProp, useRoute } from '@react-navigation/native'; import { RootStackParamList } from '../navigation/types'; type ProfileScreenRouteProp = RouteProp<RootStackParamList, 'Profile'>; const ProfileScreen = () => { const route = useRoute<ProfileScreenRouteProp>(); const { userId } = route.params; return ( <View> <Text>Profile Screen</Text> <Text>User ID: {userId}</Text> </View> ); }; export default ProfileScreen; // src/screens/HomeScreen.tsx import React from 'react'; import { Button, View, Text } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { RootStackParamList } from '../navigation/types'; import { StackNavigationProp } from '@react-navigation/stack' type HomeScreenNavigationProp = StackNavigationProp<RootStackParamList, 'Home'>; const HomeScreen = () => { const navigation = useNavigation<HomeScreenNavigationProp>(); return ( <View> <Text>Home Screen</Text> <Button title="Go to Profile" onPress={() => navigation.navigate('Profile', {userId: "42"})} /> </View> ); }; export default HomeScreen; """ ### 5.3 Standard Implement deep linking for direct navigation to specific screens. * **Do This:** Configure your React Native app to handle deep links, allowing users to navigate to specific content or sections of the app directly from external sources (e.g., emails, websites). * **Don't Do This:** Overlook deep linking, especially if your app relies on sharing content or integrating with other platforms. **Why:** Deep linking enhances user engagement, improves app discoverability, and streamlines the user experience by allowing users to bypass the app's main navigation flow and access content directly. **Example:** In "App.js" or your root component: """typescript import React, { useEffect } from 'react'; import { Linking } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import HomeScreen from './screens/HomeScreen'; import ArticleScreen from './screens/ArticleScreen'; const Stack = createStackNavigator(); function App() { useEffect(() => { const handleDeepLink = async (event) => { // Example: scheme://myapp/articles/123 const { path, queryParams } = Linking.parse(event.url); if (path.startsWith('articles/')) { const articleId = path.split('/')[1]; // Extract article id. navigation.navigate('Article', { articleId }); } }; Linking.getInitialURL().then((url) => { if (url) { handleDeepLink({ url }); } }); Linking.addEventListener('url', handleDeepLink); return () => { Linking.removeEventListener('url', handleDeepLink); }; }, []); return ( <NavigationContainer> <Stack.Navigator> <Stack.Screen name="Home" component={HomeScreen} /> <Stack.Screen name="Article" component={ArticleScreen} /> </Stack.Navigator> </NavigationContainer> ); } export default App; """ ## 6. Platform-Specific Code ### 6.1. Standard Use platform-specific extensions or the "Platform" API for handling platform-specific logic. * **Do This:** Create separate files with ".ios.js" or ".android.js" extensions for platform-specific implementations or use the "Platform" API to conditionally execute code based on the platform. * **Don't Do This:** Write complex inline conditional statements to handle platform differences. **Why:** Platform-specific code organization improves code clarity and maintainability and allows you to easily adapt the application to different platforms. **Example:** """typescript // src/components/MyComponent.ios.tsx import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; const styles = StyleSheet.create({ container: { backgroundColor: 'lightblue', // iOS-specific style }, text: { fontSize: 16, }, }); const MyComponent = () => ( <View style={styles.container}> <Text style={styles.text}>This is an iOS component.</Text> </View> ); export default MyComponent; // src/components/MyComponent.android.tsx import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; const styles = StyleSheet.create({ container: { backgroundColor: 'lightgreen', // Android-specific style }, text: { fontSize: 16, }, }); const MyComponent = () => ( <View style={styles.container}> <Text style={styles.text}>This is an Android component.</Text> </View> ); export default MyComponent; // src/components/MyComponent.tsx - Generic Component that imports from each platform import React from 'react'; import { Platform } from 'react-native'; import MyComponentIOS from './MyComponent.ios'; import MyComponentAndroid from './MyComponent.android'; const MyComponent = Platform.select({ ios: () => <MyComponentIOS />, android: () => <MyComponentAndroid />, default: () => <Text>Unsupported platform</Text>, }); export default MyComponent; """ ### 6.2. Standard Use platform-specific styling for optimal UI rendering. * **Do This:** Adjust styling based on the platform to ensure consistent and visually appealing UI across different devices. * **Don't Do This:** Use a single set of styles for all platforms, as this can lead to UI inconsistencies. **Why:** Platform-specific styling improves the user experience and ensures that the application looks and feels native on each platform. Consider using "PlatformColor" to automatically adjust for system theme changes. ## 7. Performance and Optimization ### 7.1. Standard Optimize images for mobile devices. * **Do This:** Compress images, use appropriate image formats (JPEG for photos, PNG for graphics), and resize images to the required dimensions. * **Don't Do This:** Use large, unoptimized images, as this can significantly impact application performance and storage usage. **Why:** Optimized images reduce the application's size, improve loading times, and conserve bandwidth. ### 7.2. Standard Use memoization techniques to prevent unnecessary re-renders. * **Do This:** Wrap functional components with "React.memo" to prevent re-renders when props have not changed. Use "useMemo" and "useCallback" hooks to memoize expensive calculations and function references. * **Don't Do This:** Skip memoization altogether, as this can lead to performance issues in complex applications. **Why:** Memoization optimizes rendering performance by preventing unnecessary re-renders of components and recalculations of values. **Example:** """typescript import React, { useState, useCallback, useMemo } from 'react'; interface ExpensiveComponentProps { data: number[]; } const ExpensiveComponent: React.FC<ExpensiveComponentProps> = React.memo(({ data }) => { const processedData = useMemo(() => { console.log('Processing data...'); // Simulate an expensive calculation return data.map(item => item * 2); }, [data]); return ( <View> <Text>Processed Data:</Text> {processedData.map((item, index) => ( <Text key={index}>{item}</Text> ))} </View> ); }); const MyComponent = () => { const [count, setCount] = useState(0); const [data, setData] = useState([1, 2, 3, 4, 5]); const increment = useCallback(() => { setCount(prevCount => prevCount + 1); }, []); const updateData = () => { setData(prevData => [...prevData, prevData.length + 1]); }; return ( <View> <Text>Count: {count}</Text> <Button title="Increment Count" onPress={increment} /> <Button title="Update Data" onPress={updateData} /> <ExpensiveComponent data={data} /> </View> ); }; export default MyComponent; """ ### 7.3. Standard Use FlatList or SectionList for rendering large lists of data. * **Do This:** Use "FlatList" or "SectionList" components for rendering lists with a large number of items, as these components optimize rendering performance by only rendering items that are currently visible on the screen. * **Don't Do This:** Use "ScrollView" to render large lists, as this can lead to performance issues due to rendering all items at once. **Why:** "FlatList" and "SectionList" optimize rendering performance by virtualizing the list, i.e., only rendering the items that are currently visible on the screen. ## 8. Security ### 8.1 Standard Secure API keys and sensitive information. * **Do This:** Store API keys and other sensitive information securely using environment variables or secure storage mechanisms (e.g., React Native Keychain). Never include API keys directly in the code. * **Don't Do This:** Commit API keys or other sensitive information to the source code repository. **Why:** Protecting API keys and sensitive information prevents unauthorized access to your application and its data. **Important Note**: Detailed steps on securely storing keys depend on third-party native modules designed for secure storage. General best practices involve never directly embedding keys in the code. ### 8.2 Standard Implement proper input validation and sanitization. * **Do This:** Validate and sanitize user inputs to prevent security vulnerabilities (e.g., SQL injection, cross-site scripting). Use appropriate encoding and escaping techniques when displaying user-generated content. * **Don't Do This:** Trust user input without any validation or sanitization. **Why:** Input validation and sanitization protect your application from malicious attacks and prevent data corruption. ### 8.3 Standard Secure communication with APIs * **Do This:** Always use "HTTPS" rather than "HTTP" for API communications * **Don't Do This:** Send sensitive data over unencrypted channels. **Why:** HTTPS encrypts data transmitted between the app and the server, preventing eavesdropping and man-in-the-middle attacks. ### 8.4 Standard Implement certificate pinning * **Do This
# Component Design Standards for React Native This document outlines the coding standards for React Native component design. It provides guidance on creating reusable, maintainable, and performant components. Following these standards ensures code consistency, reduces bugs, and enhances collaboration within development teams. The standards are based on the latest React Native best practices and aim to promote a modern, efficient development workflow. ## 1. Component Architecture and Structure ### 1.1. Component Types **Do This:** * Favor functional components with React Hooks over class components for new development. Function components are more concise, readable, and encourage better code organization. * Use class components only when integrating with older codebases or when very specific lifecycle methods are absolutely necessary and cannot be emulated with hooks (which is rare). **Don't Do This:** * Rely heavily on class components for new implementations. This leads to verbose code and potential performance overhead. * Mix functional and class components unnecessarily, as this introduces inconsistency and makes the codebase harder to understand. **Why:** Functional components with hooks promote better code organization, readability, and testability. They also align with the modern React paradigm, simplifying state management and side-effect handling. **Example:** """jsx // Functional Component with Hooks (Preferred) import React, { useState, useEffect } from 'react'; import { View, Text, StyleSheet, ActivityIndicator } from 'react-native'; const MyComponent = ({ text }) => { const [loading, setLoading] = useState(true); useEffect(() => { // Simulate loading data setTimeout(() => { setLoading(false); }, 1000); }, []); if (loading) { return ( <View style={styles.container}> <ActivityIndicator size="large" color="#0000ff" /> </View> ); } return ( <View style={styles.container}> <Text style={styles.text}>{text}</Text> </View> ); }; const styles = StyleSheet.create({ container: { padding: 20, backgroundColor: '#f0f0f0', borderRadius: 8, }, text: { fontSize: 16, }, }); export default MyComponent; // Class Component (Avoid for new development) import React, { Component } from 'react'; import { View, Text, StyleSheet } from 'react-native'; class MyClassComponent extends Component { constructor(props) { super(props); this.state = { message: 'Hello from Class Component!', }; } render() { return ( <View style={styles.container}> <Text style={styles.text}>{this.state.message}</Text> </View> ); } } const styles = StyleSheet.create({ container: { padding: 20, backgroundColor: '#f0f0f0', borderRadius: 8, }, text: { fontSize: 16, }, }); export default MyClassComponent; """ ### 1.2. Component Composition **Do This:** * Favor component composition over deep inheritance. Create small, focused components that can be composed together to form more complex UIs. * Use React's "children" prop or render props to pass content between components. Consider using render callbacks as needed. **Don't Do This:** * Create large, monolithic components that handle too much logic. * Rely on deep inheritance hierarchies, as this makes code harder to understand and maintain. **Why:** Component composition promotes reusability, modularity, and testability. It also aligns with the React philosophy of building UIs from small, independent components. **Example:** """jsx // Composed Components import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; // Reusable Button Component const Button = ({ title, onPress, style }) => ( <TouchableOpacity style={[styles.button, style]} onPress={onPress}> <Text style={styles.buttonText}>{title}</Text> </TouchableOpacity> ); // Card Component using Button const Card = ({ children, title }) => ( <View style={styles.card}> <Text style={styles.cardTitle}>{title}</Text> {children} </View> ); const MyScreen = () => ( <View> <Card title="Welcome!"> <Text>This is content inside the card.</Text> <Button title="Click Me" onPress={() => alert('Button Pressed!')} /> </Card> </View> ); const styles = StyleSheet.create({ card: { backgroundColor: 'white', padding: 16, margin: 8, borderRadius: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.23, shadowRadius: 2.62, elevation: 4, }, cardTitle: { fontSize: 18, fontWeight: 'bold', marginBottom: 8, }, button: { backgroundColor: '#2196F3', padding: 10, borderRadius: 5, marginTop: 10, }, buttonText: { color: 'white', textAlign: 'center', }, }); //Example of using a "render props" pattern const DataProvider = ({ url, render }) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { fetch(url) .then(response => response.json()) .then(jsonData => { setData(jsonData); setLoading(false); }) .catch(error => { console.error("Error fetching data:", error); setLoading(false); //Ensure loading is set to false even on error }); }, [url]); return loading ? <Text>Loading data...</Text> : render(data); }; // Usage: const UserList = () => ( <DataProvider url="https://jsonplaceholder.typicode.com/users" render={users => ( <ScrollView> {users.map(user => ( <Text key={user.id}>{user.name}</Text> ))} </ScrollView> )} /> ); export default MyScreen; """ ### 1.3. Folder Structure **Do This:** * Organize components based on features or modules. Create separate folders for reusable components, screens, and utility functions. * Use a consistent naming convention for files and folders. **Don't Do This:** * Place all components in a single folder. * Use inconsistent naming conventions. **Why:** A well-organized folder structure improves code discoverability, maintainability, and scalability. **Example:** """ src/ ├── components/ # Reusable components │ ├── Button/ │ │ ├── Button.js # Component implementation │ │ └── Button.styles.js # Component styles │ ├── Card/ │ │ ├── Card.js │ │ └── Card.styles.js ├── screens/ # Application screens │ ├── HomeScreen/ │ │ ├── HomeScreen.js │ │ └── HomeScreen.styles.js │ ├── ProfileScreen/ │ │ ├── ProfileScreen.js │ │ └── ProfileScreen.styles.js ├── utils/ # Utility functions │ ├── api.js │ └── helpers.js """ ## 2. Component Implementation Details ### 2.1. Props Validation **Do This:** * Use "PropTypes" or TypeScript to define the expected types and shapes of component props. This helps catch errors early and improves code documentation. * Make use of default prop values as much as possible. **Don't Do This:** * Skip prop validation. * Use "any" types recklessly in TypeScript. **Why:** Prop validation improves code reliability and helps prevent unexpected errors. **Example:** """jsx // Using PropTypes import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import PropTypes from 'prop-types'; const MyComponent = ({ name, age, isAdmin, style }) => ( <View style={[styles.container, style]}> <Text style={styles.text}>Name: {name}</Text> <Text style={styles.text}>Age: {age}</Text> <Text style={styles.text}>Is Admin: {isAdmin ? 'Yes' : 'No'}</Text> </View> ); MyComponent.propTypes = { name: PropTypes.string.isRequired, age: PropTypes.number, isAdmin: PropTypes.bool, style: PropTypes.object, }; MyComponent.defaultProps = { age: 18, isAdmin: false, }; const styles = StyleSheet.create({ container: { padding: 20, backgroundColor: '#f0f0f0', borderRadius: 8, }, text: { fontSize: 16, }, }); export default MyComponent; // Using TypeScript interface Props { name: string; age?: number; isAdmin?: boolean; style?: StyleProp<ViewStyle>; } const MyTypescriptComponent: React.FC<Props> = ({ name, age = 18, isAdmin = false, style }) => ( <View style={[styles.container, style]}> <Text style={styles.text}>Name: {name}</Text> <Text style={styles.text}>Age: {age}</Text> <Text style={styles.text}>Is Admin: {isAdmin ? 'Yes' : 'No'}</Text> </View> ); const styles = StyleSheet.create({ container: { padding: 20, backgroundColor: '#f0f0f0', borderRadius: 8, }, text: { fontSize: 16, }, }); export default MyTypescriptComponent; """ ### 2.2. State Management **Do This:** * Use React's "useState" hook for local component state. * Use Context API, Redux, Zustand, or similar libraries for application-wide state management. * Consider using "useReducer" hook for more complex state logic within a component. **Don't Do This:** * Overuse global state management for local component state. * Mutate state directly (e.g., "this.state.value = newValue"). Always use "setState" or the state updater function provided by "useState". **Why:** Proper state management ensures predictable data flow and simplifies debugging. Avoid unnecessary re-renders that can occur with poorly managed state. **Example:** """jsx // Using useState hook import React, { useState } from 'react'; import { View, Text, Button, StyleSheet } from 'react-native'; const CounterComponent = () => { const [count, setCount] = useState(0); const increment = () => { setCount(prevCount => prevCount + 1); // Functional update form }; const decrement = () => { setCount(count - 1); }; return ( <View style={styles.container}> <Text style={styles.text}>Count: {count}</Text> <Button title="Increment" onPress={increment} /> <Button title="Decrement" onPress={decrement} /> </View> ); }; const styles = StyleSheet.create({ container: { padding: 20, backgroundColor: '#f0f0f0', borderRadius: 8, }, text: { fontSize: 16, }, }); // Using useReducer hook import React, { useReducer } from 'react'; import { View, Text, Button, StyleSheet } from 'react-native'; const reducer = (state, action) => { switch (action.type) { case 'INCREMENT': return { count: state.count + 1 }; case 'DECREMENT': return { count: state.count - 1 }; default: return state; } }; const CounterWithReducer = () => { const [state, dispatch] = useReducer(reducer, { count: 0 }); return ( <View style={styles.container}> <Text style={styles.text}>Count: {state.count}</Text> <Button title="Increment" onPress={() => dispatch({ type: 'INCREMENT' })} /> <Button title="Decrement" onPress={() => dispatch({ type: 'DECREMENT' })} /> </View> ); }; """ ### 2.3. Styling **Do This:** * Use React Native's "StyleSheet" API for defining styles. This helps to enforce consistency and improve performance. * Keep styles close to the component they are styling (co-location). * Leverage style inheritance and composition to avoid duplication. * Use themes and style variables for consistent look and feel. **Don't Do This:** * Use inline styles excessively, as this makes code harder to read and maintain. * Hardcode style values throughout the application. **Why:** Consistent styling improves the user experience and makes the application easier to maintain. **Example:** """jsx // Using StyleSheet import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; const MyComponent = ({ text }) => ( <View style={styles.container}> <Text style={styles.text}>{text}</Text> </View> ); const styles = StyleSheet.create({ container: { padding: 20, backgroundColor: '#f0f0f0', borderRadius: 8, alignItems: 'center', }, text: { fontSize: 16, color: '#333', }, }); // Example of Themes: const theme = { colors: { primary: '#2196F3', secondary: '#607D8B', text: '#333', background: '#f0f0f0' }, spacing: { small: 8, medium: 16, large: 24 }, typography: { fontSize: { small: 14, medium: 16, large: 18 } } }; //In a separate file, create a context import React from 'react'; const ThemeContext = React.createContext(theme); export default ThemeContext; //A custom hook to consume the theme: import { useContext } from 'react'; import ThemeContext from './ThemeContext'; const useTheme = () => useContext(ThemeContext); export default useTheme; //Then in a component import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import useTheme from './useTheme'; //Import the custom hook const ThemedComponent = () => { const theme = useTheme(); //Consume the theme return ( <View style={[styles.container, { backgroundColor: theme.colors.background }]}> <Text style={[styles.text, { color: theme.colors.text }]}>Themed Text</Text> </View> ); }; const styles = StyleSheet.create({ container: { padding: 20, borderRadius: 8, alignItems: 'center', }, text: { fontSize: 16, }, }); export default ThemedComponent; export default MyComponent; """ ### 2.4. Handling Asynchronous Operations **Do This:** * Use "async/await" syntax for handling asynchronous operations. * Implement proper error handling using "try/catch" blocks. * Show loading indicators while waiting for asynchronous operations to complete. **Don't Do This:** * Use callbacks excessively, as this can lead to callback hell. * Ignore potential errors during asynchronous operations. **Why:** Proper handling of asynchronous operations ensures a smooth and responsive user experience. **Example:** """jsx // Using async/await import React, { useState, useEffect } from 'react'; import { View, Text, Button, ActivityIndicator, StyleSheet } from 'react-native'; const DataFetchingComponent = () => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchData = async () => { try { const response = await fetch('https://jsonplaceholder.typicode.com/todos/1'); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } const json = await response.json(); setData(json); } catch (e) { setError(e); console.error("Fetching error:", e); // Consider using a logging service in production } finally { setLoading(false); } }; fetchData(); }, []); if (loading) { return ( <View style={styles.container}> <ActivityIndicator size="large" color="#0000ff" /> </View> ); } if (error) { return ( <View style={styles.container}> <Text style={styles.text}>Error: {error.message}</Text> </View> ); } return ( <View style={styles.container}> <Text style={styles.text}>Title: {data.title}</Text> <Text style={styles.text}>Completed: {data.completed ? 'Yes' : 'No'}</Text> </View> ); }; const styles = StyleSheet.create({ container: { padding: 20, backgroundColor: '#f0f0f0', borderRadius: 8, alignItems: 'center', }, text: { fontSize: 16, }, }); export default DataFetchingComponent; """ ## 3. Performance Optimization ### 3.1. Memoization **Do This:** * Use "React.memo" to memoize functional components and prevent unnecessary re-renders. * Use "useMemo" and "useCallback" hooks to memoize values and functions passed as props. **Don't Do This:** * Memoize components indiscriminately, as this can add unnecessary overhead. * Pass new object literals or inline functions as props to memoized components. **Why:** Memoization can improve performance by preventing unnecessary re-renders of components, particularly when dealing with complex UIs or expensive calculations. **Example:** """jsx // Using React.memo import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; const MyComponent = ({ text }) => { console.log('MyComponent rendered'); // For debugging render frequency return ( <View style={styles.container}> <Text style={styles.text}>{text}</Text> </View> ); }; const styles = StyleSheet.create({ container: { padding: 20, backgroundColor: '#f0f0f0', borderRadius: 8, margin: 10, }, text: { fontSize: 16, }, }); const MemoizedComponent = React.memo(MyComponent); //Memoize the component export default MemoizedComponent; // Using useCallback and useMemo import React, { useState, useCallback, useMemo } from 'react'; import { View, Text, Button, StyleSheet } from 'react-native'; const ExpensiveComponent = React.memo(({ data, onClick }) => { console.log('ExpensiveComponent rendered'); return ( <Button title={"Item: ${data.value}"} onPress={onClick} /> ); }); const MyParentComponent = () => { const [count, setCount] = useState(0); const data = useMemo(() => ({ value: count }), [count]); //Memoize data const handleClick = useCallback(() => { // Memoize the callback alert("Clicked item ${count}"); }, [count]); return ( <View> <Text>Count: {count}</Text> <Button title="Increment" onPress={() => setCount(count + 1)} /> <ExpensiveComponent data={data} onClick={handleClick} /> </View> ); }; """ ### 3.2. Optimizing List Rendering **Do This:** * Use "FlatList" or "SectionList" components for rendering large lists. These components virtualize the list and only render items that are currently visible on the screen. * Provide a unique "key" prop for each item in the list. * Implement "getItemLayout" prop to improve scrolling performance. **Don't Do This:** * Use "ScrollView" for rendering large lists, as this can cause performance issues. * Use the index as the key prop. **Why:** "FlatList" and "SectionList" optimize list rendering by virtualizing the list and only rendering items that are currently visible on the screen. **Example:** """jsx // Using FlatList import React from 'react'; import { FlatList, View, Text, StyleSheet } from 'react-native'; const data = Array.from({ length: 100 }, (_, i) => ({ id: i.toString(), value: "Item ${i + 1}" })); const Item = ({ title }) => ( <View style={styles.item}> <Text style={styles.title}>{title}</Text> </View> ); const MyListComponent = () => { const renderItem = ({ item }) => ( <Item title={item.value} /> ); return ( <FlatList data={data} renderItem={renderItem} keyExtractor={item => item.id} getItemLayout={(data, index) => ({length: 50, offset: 50 * index, index})} //Example, assuming item height is fixed at 50 /> ); }; const styles = StyleSheet.create({ item: { backgroundColor: '#f9c2ff', padding: 20, marginVertical: 8, marginHorizontal: 16, }, title: { fontSize: 16, }, }); export default MyListComponent; """ ### 3.3 Image Optimization **Do This:** * Use optimized image formats (e.g., WebP) to reduce image size. * Use "Image" component's "resizeMode" prop effectively (cover, contain, stretch, repeat, center). * Consider using a library like "react-native-fast-image" for improved image caching and performance. **Don't Do This:** * Use large, unoptimized images. * Load images unnecessarily. **Why:** Optimized images reduce the application's footprint, improve loading times, and enhance the user experience. **Example:** """jsx // Using Image component import React from 'react'; import { Image, View, StyleSheet } from 'react-native'; const MyImageComponent = () => ( <View style={styles.container}> <Image style={styles.image} source={{ uri: 'https://via.placeholder.com/150', }} resizeMode="cover" //Choose an appropriate resizeMode /> </View> ); const styles = StyleSheet.create({ container: { padding: 20, }, image: { width: 150, height: 150, }, }); export default MyImageComponent; //Using FastImage import FastImage from 'react-native-fast-image' const FastImageComponent = () => ( <View style={styles.container}> <FastImage style={styles.image} source={{ uri: 'https://via.placeholder.com/150', headers: { Authorization: 'someAuthToken' }, priority: FastImage.priority.normal, }} resizeMode={FastImage.resizeMode.contain} /> </View> ) """ ## 4. Security Best Practices ### 4.1. Data Validation and Sanitization **Do This:** * Validate and sanitize all user inputs to prevent injection attacks (e.g., SQL injection, XSS). * Use secure storage mechanisms for storing sensitive data (e.g., passwords, API keys). **Don't Do This:** * Trust user inputs without validation. * Store sensitive data in plain text. **Why:** Data validation and sanitization prevent malicious code from being injected into the application. Secure storage protects sensitive data from unauthorized access. ### 4.2. Secure Communication **Do This:** * Use HTTPS for all network communication to encrypt data in transit. * Implement proper authentication and authorization mechanisms to protect API endpoints. **Don't Do This:** * Use HTTP for transmitting sensitive data. * Expose API endpoints without authentication. **Why:** Secure communication protects data from eavesdropping and tampering. Proper authentication and authorization prevent unauthorized access to API endpoints. ### 4.3. Dependency Management **Do This:** * Keep dependencies up-to-date to patch security vulnerabilities. * Review dependencies for potential security risks. * Use a package manager (e.g., npm, yarn) to manage dependencies. **Don't Do This:** * Use outdated dependencies. * Install dependencies from untrusted sources. **Why:** Up-to-date dependencies reduce the risk of security vulnerabilities. Reviewing dependencies helps to identify and mitigate potential security risks. ## 5. Testing ### 5.1. Unit Testing **Do This:** * Write unit tests for individual components and functions to ensure they work as expected. * Use a testing framework (e.g., Jest, Mocha) to run unit tests. **Don't Do This:** * Skip unit testing. * Write tests that are too complex or tightly coupled to the implementation. **Why:** Unit tests provide a safety net for code changes and help to prevent regressions. ### 5.2. Integration Testing **Do This:** * Write integration tests to ensure that components work together correctly. * Use a testing framework (e.g., Detox, Appium) to run integration tests. **Don't Do This:** * Skip integration testing. * Write tests that are too brittle or rely on specific implementation details. **Why:** Integration tests ensure that the different parts of the application work together correctly. ### 5.3 Mocking **Do This:** * Mock external API calls * Mock native dependencies such as file system access or bluetooth * Use mocking libraries such as jest.mock or react-native-reanimated mocks to reduce build times and improve test isolation. **Don't Do This:** * Skip mocking key dependencies that will block testing. * Over mock components that makes tests unnecessary brittle. **Why:** Mocking dependencies decreases test times and improves granularity. By adhering to these component design standards, development teams can create React Native applications that are maintainable, performant, secure, and testable. These guidelines are intended to be flexible and adaptable to the specific needs of individual projects.
# State Management Standards for React Native This document outlines the coding standards for state management in React Native applications. It provides guidelines for choosing appropriate state management solutions, structuring data flow, and writing maintainable and performant code. ## 1. Guiding Principles * **Single Source of Truth:** Each piece of application state should have a single, authoritative source. Avoid duplicating state across multiple components, as this leads to inconsistencies and difficulties in maintaining data integrity. * **Predictable State Updates:** State updates should be predictable and traceable. Favor immutable data structures and pure functions to ensure that state changes are easily understood and debugged. * **Component Isolation:** Promote component isolation by only providing components with the data they require. Use techniques like prop drilling through context or selectors with global state management solutions to avoid unnecessary re-renders. * **Performance:** Optimize state updates and rendering to ensure a smooth user experience. Use memoization techniques, lazy loading of data, and efficient data structures to minimize performance bottlenecks. * **Testability:** Adhere to principles that allow for adequate testing on state management and data handling logic. * **Data serialization:** Ensure proper serialzation/deserialization of local storage and network layer for data consistency. ## 2. State Management Options Choosing the right state management solution is crucial for building scalable React Native applications. Consider the following options: ### 2.1 "useState" and "useReducer" (Local Component State) * **Use Case:** Managing simple, component-local state. * **Do This:** * Use "useState" for simple state variables that change infrequently. * Use "useReducer" for managing more complex state logic, especially when the next state depends on the previous state. * **Don't Do This:** * Overuse "useState" in deeply nested components where the state could be managed more effectively at a higher level. * Mutate state directly. Always use the setter function provided by "useState" or "useReducer". """javascript // useState example import React, { useState } from 'react'; import { View, Text, Button } from 'react-native'; const Counter = () => { const [count, setCount] = useState(0); return ( <View> <Text>Count: {count}</Text> <Button title="Increment" onPress={() => setCount(count + 1)} /> </View> ); }; export default Counter; // useReducer example import React, { useReducer } from 'react'; import { View, Text, Button } from 'react-native'; const initialState = { count: 0 }; const reducer = (state, action) => { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: return state; } }; const CounterReducer = () => { const [state, dispatch] = useReducer(reducer, initialState); return ( <View> <Text>Count: {state.count}</Text> <Button title="Increment" onPress={() => dispatch({ type: 'increment' })} /> <Button title="Decrement" onPress={() => dispatch({ type: 'decrement' })} /> </View> ); }; export default CounterReducer; """ #### 2.1.1 "useReducer" for Complex State Transitions For state logic that requires more structured updates, utilize "useReducer". It allows you to define a reducer function that dictates how the state should change based on actions. """javascript import React, { useReducer } from 'react'; import { View, Text, Button } from 'react-native'; const initialTodos = [ { id: 1, text: 'Learn Reducer', completed: false }, ]; const todosReducer = (state, action) => { switch (action.type) { case 'ADD_TODO': return [...state, { id: Date.now(), text: action.text, completed: false }]; case 'TOGGLE_TODO': return state.map((todo) => todo.id === action.id ? { ...todo, completed: !todo.completed } : todo ); default: return state; } }; const Todos = () => { const [todos, dispatch] = useReducer(todosReducer, initialTodos); const addTodo = () => { dispatch({ type: 'ADD_TODO', text: 'New Todo' }); }; const toggleTodo = (id) => { dispatch({ type: 'TOGGLE_TODO', id }); }; return ( <View> {todos.map((todo) => ( <Button key={todo.id} title={todo.text} onPress={() => toggleTodo(todo.id)} /> ))} <Button title="Add Todo" onPress={addTodo} /> </View> ); }; """ #### 2.1.2 Avoiding Direct Mutation Always return a new state object from the reducer. Avoid direct mutation of the state, as it can lead to unexpected behavior and performance problems. """javascript // Do this: const reducer = (state, action) => { return { ...state, value: action.payload }; // Create a new object }; // Don't do this: const reducer = (state, action) => { state.value = action.payload; // Direct mutation return state; }; """ ### 2.2 Context API (Global Component State) * **Use Case:** Sharing state between components without prop drilling. Can also be used as simplistic global state management and dependency injection. * **Do This:** * Use "Context.Provider" to wrap the part of your component tree that needs access to the context. * Use "useContext" hook to consume the context value in functional components. * Create custom hooks that wrap "useContext" for more maintainable and reusable code. * **Don't Do This:** * Store large amounts of rapidly changing data in context, as it can cause unnecessary re-renders. Consider using a more optimized global state management solution such as Redux or Zustand for such cases. * Mutate the context value directly. Always provide a mechanism for updating the context value through a provider function. """javascript // Create a context import React, { createContext, useState, useContext } from 'react'; import { View, Text, Button } from 'react-native'; const ThemeContext = createContext(); // Create a provider const ThemeProvider = ({ children }) => { const [theme, setTheme] = useState('light'); const toggleTheme = () => { setTheme(theme === 'light' ? 'dark' : 'light'); }; return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); }; // Custom Hook that wraps useContext const useTheme = () => { return useContext(ThemeContext); }; // Consumer component const ThemedComponent = () => { const { theme, toggleTheme } = useTheme(); return ( <View style={{ backgroundColor: theme === 'light' ? 'white' : 'black' }}> <Text style={{ color: theme === 'light' ? 'black' : 'white' }}> Current Theme: {theme} </Text> <Button title="Toggle Theme" onPress={toggleTheme} /> </View> ); }; // App component const App = () => { return ( <ThemeProvider> <ThemedComponent /> </ThemeProvider> ); }; export default App; """ #### 2.2.1 Value stability for Context * **Do This:** Ensure stability of provided value for context. For example, when the context becomes too large, provide a memoized function. """javascript // Stable context provider, wrapping theme and toggler together. const ThemeProvider = ({ children }) => { const [theme, setTheme] = useState('light'); // Memoize toggleTheme to prevent unnecessary re-renders const toggleTheme = useCallback(() => { setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light')); }, []); const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]); return ( <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider> ); }; """ ### 2.3 Redux (Global State Management) * **Use Case:** Managing complex application state, especially when state is shared across many components and requires predictable updates. * **Do This:** * Use Redux Toolkit to simplify Redux setup and reduce boilerplate code. * Structure your state into logical slices. * Use selectors to efficiently derive data from the store. * Favor asynchronous actions with Redux Thunk or Redux Saga for handling side effects. * **Don't Do This:** * Use Redux for simple applications where component-local state or Context API would suffice. * Mutate state directly in reducers. Always return a new state object. * Put non-serializable data in the Redux store (functions, class instances, etc.). """javascript // Redux Toolkit setup import { configureStore, createSlice } from '@reduxjs/toolkit'; import { Provider, useDispatch, useSelector } from 'react-redux'; import React from 'react'; import { View, Text, Button } from 'react-native'; // Create a slice const counterSlice = createSlice({ name: 'counter', initialState: { value: 0, }, reducers: { increment: (state) => { state.value += 1; }, decrement: (state) => { state.value -= 1; }, }, }); // Export actions and reducer export const { increment, decrement } = counterSlice.actions; const counterReducer = counterSlice.reducer; // Create the store const store = configureStore({ reducer: { counter: counterReducer, }, }); // Component using Redux const CounterRedux = () => { const count = useSelector((state) => state.counter.value); const dispatch = useDispatch(); return ( <View> <Text>Count: {count}</Text> <Button title="Increment" onPress={() => dispatch(increment())} /> <Button title="Decrement" onPress={() => dispatch(decrement())} /> </View> ); }; // Root component const App = () => { return ( <Provider store={store}> <CounterRedux /> </Provider> ); }; export default App; """ #### 2.3.1 Redux Thunk for Asynchronous Actions Use Redux Thunk middleware to handle asynchronous actions, such as fetching data from an API. """javascript // Example using Redux Thunk import { createAsyncThunk } from '@reduxjs/toolkit'; // Async thunk action export const fetchUser = createAsyncThunk( 'user/fetchUser', async (userId, { rejectWithValue }) => { try { const response = await fetch("https://api.example.com/users/${userId}"); if (!response.ok) { // Handle HTTP errors return rejectWithValue("HTTP error! status: ${response.status}"); } const data = await response.json(); return data; } catch (error) { // Handle network errors return rejectWithValue(error.message); } } ); // Update the slice const userSlice = createSlice({ name: 'user', initialState: { user: null, loading: false, error: null }, reducers: {}, extraReducers: (builder) => { builder .addCase(fetchUser.pending, (state) => { state.loading = true; state.error = null; }) .addCase(fetchUser.fulfilled, (state, action) => { state.loading = false; state.user = action.payload; }) .addCase(fetchUser.rejected, (state, action) => { state.loading = false; state.error = action.payload; // Use custom error message from the thunk }); }, }); """ #### 2.3.2 Selectors Use selectors (e.g., Reselect) to derive data from the Redux store efficiently. Selectors memoize their results, preventing unnecessary recomputations and re-renders. """javascript // Example using Reselect import { createSelector } from '@reduxjs/toolkit'; import { useSelector } from 'react-redux'; const selectCounterValue = (state) => state.counter.value; export const selectDoubleCounterValue = createSelector( [selectCounterValue], (value) => value * 2 ); const MyComponent = () => { const doubleCount = useSelector(selectDoubleCounterValue); return <Text>Double Count: {doubleCount}</Text>; }; """ ### 2.4 Zustand (Global State Management) * **Use Case:** Simpler alternative to Redux for global state management with a minimal API. * **Do This:** * Define your store using the "create" function. * Use the "set" function to update state. * Use selectors to access state in components. * **Don't Do This:** * Overcomplicate your store logic. Zustand is designed to be simple. * Mutate state directly. Always use the "set" function. """javascript // Zustand store import create from 'zustand'; import { View, Text, Button } from 'react-native'; import React from 'react'; const useStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), })); // Component using Zustand const CounterZustand = () => { const { count, increment, decrement } = useStore((state) => ({ count: state.count, increment: state.increment, decrement: state.decrement, })); return ( <View> <Text>Count: {count}</Text> <Button title="Increment" onPress={increment} /> <Button title="Decrement" onPress={decrement} /> </View> ); }; export default CounterZustand; """ ### 2.5 Jotai (Global State Management) * **Use Case:** Atomic state management library, suitable for fine-grained state updates with optimized re-renders. * **Do This:** * Define atoms for individual pieces of state. * Use the "useAtom" hook to read and update atoms in components. * Compose atoms to derive computed values. * **Don't Do This:** * Create too many atoms for closely related state, which can make your code harder to understand. * Overuse derived atoms, which can lead to performance issues if they are not properly memoized. """javascript // Jotai atoms import { atom, useAtom } from 'jotai'; import { View, Text, Button } from 'react-native'; import React from 'react'; const countAtom = atom(0); const CounterJotai = () => { const [count, setCount] = useAtom(countAtom); return ( <View> <Text>Count: {count}</Text> <Button title="Increment" onPress={() => setCount(count + 1)} /> <Button title="Decrement" onPress={() => setCount(count - 1)} /> </View> ); }; export default CounterJotai; """ #### 2.5.1 Derived atoms in Jotai Derived atoms are a powerful way to compute new atoms based on other atoms. """javascript import { atom, useAtom } from 'jotai'; const basePriceAtom = atom(10); const discountAtom = atom(0.2); const discountedPriceAtom = atom((get) => { const basePrice = get(basePriceAtom); const discount = get(discountAtom); return basePrice * (1 - discount); }); const PriceDisplay = () => { const [discountedPrice] = useAtom(discountedPriceAtom); return ( <Text>Discounted Price: {discountedPrice}</Text> ); }; """ ## 3. Data Fetching and API Integration * **Use Case:** Data fetching from REST APIs and GraphQL APIs. * **Do This:** * Use libraries like "axios" or the built-in "fetch" API for making HTTP requests. * Use "useEffect" hook to fetch data when the component mounts. * Implement proper error handling and loading states for a better user experience. * Use caching mechanisms to avoid redundant API calls. * Consider using libraries like "react-query" or "swr" for simplified data fetching and caching. * **Don't Do This:** * Make API calls directly in the render function, as this can lead to performance issues and infinite loops. * Ignore error handling, which can result in unexpected app behavior. * Store sensitive API keys directly in the code. Use environment variables. """javascript // Data fetching with useEffect import React, { useState, useEffect } from 'react'; import { View, Text } from 'react-native'; import axios from 'axios'; const DataFetcher = () => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchData = async () => { try { const result = await axios('https://api.example.com/data'); setData(result.data); setLoading(false); } catch (e) { setError(e); setLoading(false); } }; fetchData(); }, []); // Empty dependency array to run only on mount if (loading) { return <Text>Loading...</Text>; } if (error) { return <Text>Error: {error.message}</Text>; } return ( <View> <Text>Data: {JSON.stringify(data)}</Text> </View> ); }; export default DataFetcher; """ #### 3.1.1 Using "react-query" for Data Fetching and Caching "react-query" simplifies data fetching, caching, and updating in React applications. """javascript import { useQuery } from 'react-query'; import { View, Text } from 'react-native'; const fetchData = async () => { const response = await fetch('https://api.example.com/data'); if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }; const DataFetcherWithReactQuery = () => { const { data, isLoading, error } = useQuery('myData', fetchData); if (isLoading) { return <Text>Loading...</Text>; } if (error) { return <Text>Error: {error.message}</Text>; } return ( <View> <Text>Data: {JSON.stringify(data)}</Text> </View> ); }; export default DataFetcherWithReactQuery; """ ## 4. Updating and Transforming State * **Use Case:** Correctly updating and transforming state in React Native components. Focus on immutability. * **Do This:** * Use immutable update patterns to avoid unexpected side effects. * When updating arrays or objects in state, create new copies instead of modifying the original objects. * Use the spread operator ("...") to create shallow copies of objects and arrays. * Use libraries like "immer" for simplifying immutable updates with deeply nested objects. * **Don't Do This:** * Mutate state directly (e.g., "state.push(newItem)"). * Forget to update nested objects immutably. """javascript // Immutable updates import React, { useState } from 'react'; import { View, Text, Button } from 'react-native'; const ImmutableUpdate = () => { const [items, setItems] = useState([{ id: 1, name: 'Item 1' }]); const addItem = () => { setItems([...items, { id: Date.now(), name: 'New Item' }]); }; const updateItem = (id, newName) => { setItems( items.map((item) => (item.id === id ? { ...item, name: newName } : item)) ); }; return ( <View> {items.map((item) => ( <Text key={item.id}>{item.name}</Text> ))} <Button title="Add Item" onPress={addItem} /> <Button title="Update Item 1" onPress={() => updateItem(1, 'Updated Item')} /> </View> ); }; export default ImmutableUpdate; """ ### 4.1 Using Immer for Complex Immutable Updates "immer" simplifies working with immutable data structures, especially when dealing with deeply nested objects. """javascript import React, { useState } from 'react'; import { View, Text, Button } from 'react-native'; import { useImmer } from 'use-immer'; const ImmerExample = () => { const [state, updateState] = useImmer({ user: { name: 'Alice', address: { street: '123 Main St', city: 'Anytown', }, }, }); const updateCity = () => { updateState((draft) => { draft.user.address.city = 'Newtown'; }); }; return ( <View> <Text>Name: {state.user.name}</Text> <Text>City: {state.user.address.city}</Text> <Button title="Update City" onPress={updateCity} /> </View> ); }; """ ## 5. Performance Optimization * **Use Case:** Optimizing React Native state management for smooth UI. * **Do This:** * Use "React.memo" to prevent unnecessary re-renders of components. * Use "useCallback" to memoize event handlers. * Use "useMemo" to memoize expensive computations. * Virtualize long lists with "FlatList" or "SectionList" to improve scrolling performance. Avoid "map" on large arrays where possible. * Avoid unnecessary state updates by comparing previous and next values. * **Don't Do This:** * Overuse "React.memo", "useCallback", and "useMemo", as they can add unnecessary overhead if not used judiciously. * Update state frequently with large amounts of data, as it can cause performance bottlenecks. """javascript // Memoization with React.memo import React from 'react'; import { View, Text } from 'react-native'; const MyComponent = ({ data }) => { console.log('MyComponent rendered'); return <Text>Data: {data}</Text>; }; const MemoizedComponent = React.memo(MyComponent); // Memoization with useCallback import React, { useCallback } from 'react'; import { Button } from 'react-native'; const MyButton = ({ onClick }) => { console.log('MyButton rendered'); return <Button title="Click Me" onPress={onClick} />; }; const MemoizedButton = React.memo(MyButton); const ParentComponent = () => { const handleClick = useCallback(() => { console.log('Button clicked'); }, []); return <MemoizedButton onClick={handleClick} />; }; // Virtualized list import React from 'react'; import { FlatList, Text, View } from 'react-native'; const data = Array.from({ length: 1000 }, (_, i) => ({ id: i, text: "Item ${i}" })); const Item = React.memo(({ text }) => { console.log("Item ${text} rendered"); return <Text>{text}</Text>; }); const MyList = () => { const renderItem = ({ item }) => <Item text={item.text} />; return ( <FlatList data={data} renderItem={renderItem} keyExtractor={(item) => item.id.toString()} /> ); }; """ ## 6. Testing State Management * **Use Case:** Testing React Native state management code thoroughly. * **Do This:** * Write unit tests for reducers, actions, and selectors. * Use mock stores and providers to isolate components during testing. * Test side effects (e.g., API calls) using mock libraries like "axios-mock-adapter". * Write integration tests to verify the interaction between components and state management logic. * Mock API calls for testing the data fetching and success and failure states. * **Don't Do This:** * Skip testing state management logic. * Write brittle tests that rely on implementation details rather than behavior. """javascript // Example using Jest and React Testing Library with React Hook Form import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react-native'; import { useForm } from 'react-hook-form'; // Test component const MyForm = () => { const { register, handleSubmit, formState: { errors } } = useForm(); const onSubmit = (data) => { alert(JSON.stringify(data)); }; return ( <View> <TextInput placeholder="Name" {...register('name', { required: 'Name is required' })} /> {errors.name && <Text>{errors.name.message}</Text>} <Button title="Submit" onPress={handleSubmit(onSubmit)} /> </View> ); }; // Test case it('should display an error message if name is not entered', () => { render(<MyForm />); fireEvent.press(screen.getByText('Submit')); expect(screen.getByText('Name is required')).toBeTruthy(); }); it('should submit the form with valid data', () => { const alertMock = jest.spyOn(window, 'alert'); // Mock the alert function render(<MyForm />); const nameInput = screen.getByPlaceholderText('Name'); fireEvent.changeText(nameInput, 'John Doe'); fireEvent.press(screen.getByText('Submit')); expect(alertMock).toHaveBeenCalledWith(JSON.stringify({ name: 'John Doe' })); // Check if the alert was called with the correct data alertMock.mockRestore(); // Restore alert to original function }); """ ## 7. Security Considerations * **Use Case:** State Management security standards for React Native * **Do This:** * Avoid storing sensitive data directly in state management solutions that can be easily accessed (e.g., tokens, passwords, etc.). * Encrypt sensitive data before storing it in local storage or global state. * Sanitize user inputs before updating the state to prevent XSS attacks. * Implement proper authentication and authorization mechanisms to protect sensitive API endpoints. * **Don't Do This:** * Store sensitive information in plain text. * Trust user input without validation and sanitization. * Expose sensitive data in client-side code. ## 8. Conclusion Adhering to these standards can significantly improve the quality, maintainability, and performance of React Native applications. By selecting the right architecture and tools, and writing clean, testable code, developers can create robust and scalable solutions that deliver a great user experience.