# 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 My App;
};
const ProductList = ({ products }) => {
return (
}
keyExtractor={item => item.id}
/>
);
};
const ProductItem = ({ product }) => {
return {product.name}
}
const MainScreen = ({ products }) => {
return (
);
};
// Bad: One giant component
const MainScreenUnoptimized = ({ products }) => {
return (
My App
{item.name}}
keyExtractor={item => item.id}
/>
);
};
"""
### 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 {data.value};
});
// Alternatively, use React.PureComponent for class components
class MyPureComponent extends React.PureComponent {
render() {
console.log('MyPureComponent rendered');
return {this.props.data.value};
}
}
// 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 (
setCount(c => c + 1)} />
);
};
"""
### 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 }) => (
{item.text}
);
const App = () => {
return (
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 (
setLoaded(true)}
//Show placeholder until the image is loaded completely
{...(loaded ? {} : {fallback: true})}
/>
);
};
// Usage:
"""
### 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 (
);
};
"""
## 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 (
Fading 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 (
{processedData.map(item => {item})}
);
};
"""
### 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 (
{module ? : Loading...}
);
};
"""
### 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 (
);
};
"""
### 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 My Component;
};
"""
### 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(
Length {count}
);
};
"""
## 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() {
@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.
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.
# 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.
# Testing Methodologies Standards for React Native This document outlines the testing methodologies standards for React Native applications. These guidelines ensure code quality, maintainability, and reliability by establishing best practices for unit, integration, and end-to-end testing within the React Native ecosystem. ## 1. General Testing Principles ### 1.1. Test-Driven Development (TDD) & Behavior-Driven Development (BDD) **Do This:** * Consider adopting TDD or BDD principles where appropriate. Write tests *before* implementing the code. This helps clarify requirements and design a more testable architecture from the outset. **Don't Do This:** * De-prioritize testing altogether or defer it until the end of a feature's development. This often results in poorly tested code and increased debugging effort. **Why:** * TDD/BDD can significantly improve code quality by driving design and ensuring features meet requirements early. It reduces defects and simplifies debugging. ### 1.2. Test Pyramid **Do This:** * Adhere to the test pyramid concept: * **Unit Tests (Top of Pyramid, Largest Quantity):** Focus on individual components and functions. * **Integration Tests (Middle of Pyramid, Moderate Quantity):** Verify interactions between multiple components. * **End-to-End (E2E) Tests (Bottom of Pyramid, Smallest Quantity):** Simulate real user flows across the entire application. **Don't Do This:** * Over-rely on E2E tests at the expense of unit and integration tests. E2E tests are slower and more brittle which leads to longer build times and decreased developer productivity. **Why:** * The test pyramid provides a balanced approach to testing, maximizing coverage while minimizing test execution time and maintenance effort. ### 1.3. Code Coverage **Do This:** * Aim for high code coverage (80%+), but focus on *meaningful* tests. Test all core logic, edge cases, and error conditions. **Don't Do This:** * Strive for 100% code coverage at all costs. Writing superficial tests solely to increase coverage isn't productive and can create maintenance overhead. * Ignore code coverage reports. Use them to identify untested areas and prioritize test development. **Why:** * Code coverage metrics help identify untested areas which potentially harbor bugs but should not be treated as the sole measure of test effectiveness. Prioritize writing tests that address critical functionality. ## 2. Unit Testing ### 2.1. Scope **Do This:** * Unit tests should focus on individual components, functions, or modules in isolation. * Mock dependencies (e.g., API calls, data stores, context providers, native modules) to isolate the unit under test. **Don't Do This:** * Allow side effects or external dependencies to influence unit tests. This makes tests unreliable and difficult to debug. **Why:** * Isolating the unit under test ensures that failures are localized, and tests run quickly. This promotes maintainability and rapid feedback. ### 2.2. Tools & Libraries **Do This:** * Use Jest as the primary testing framework. It is the recommended and most widely supported framework for React Native. * Use React Native Testing Library to test React components in a user-centric way. **Don't Do This:** * Use shallow rendering excessively. Prioritize testing the user-visible aspects of your components using React Native Testing Library or similar. **Why:** * Jest provides a comprehensive testing environment with mocking, assertions, and snapshot testing capabilities. React Native Testing Library encourages testing components from the user's perspective. ### 2.3. Example: Unit Testing a Simple Component """javascript // src/components/Greeting.js import React from 'react'; import { Text, View } from 'react-native'; const Greeting = ({ name }) => { return ( <View> <Text>Hello, {name}!</Text> </View> ); }; export default Greeting; """ """javascript // src/components/Greeting.test.js import React from 'react'; import { render, screen } from '@testing-library/react-native'; import Greeting from './Greeting'; describe('Greeting Component', () => { it('renders a greeting message with the provided name', () => { render(<Greeting name="John" />); const greetingElement = screen.getByText('Hello, John!'); expect(greetingElement).toBeDefined(); }); it('renders default greeting if no name is provided', () => { render(<Greeting />); const greetingElement = screen.getByText('Hello, undefined!'); //Avoid this: rely on proptypes for default values }); }); """ ### 2.4. Mocking **Do This:** * Use Jest's mocking capabilities to isolate the unit under test. Use "jest.mock()" to mock modules and "jest.fn()" to create mock functions. For mocking React Context: create a mock context provider. **Don't Do This:** * Over-mock. Only mock dependencies that prevent you from testing the core logic of the unit. * Mock implementation details. Mock only the public interface of dependencies. **Why:** * Mocking ensures that tests are deterministic and independent of external factors. It also allows you to simulate different scenarios, such as error states. """javascript // Example: Mocking an API call // src/api/userApi.js const fetchUserData = async (userId) => { const response = await fetch("https://api.example.com/users/${userId}"); const data = await response.json(); return data; }; export default fetchUserData; // src/components/UserProfile.js import React, { useState, useEffect } from 'react'; import { Text, View } from 'react-native'; import fetchUserData from '../api/userApi'; const UserProfile = ({ userId }) => { const [user, setUser] = useState(null); useEffect(() => { const loadUserData = async () => { const userData = await fetchUserData(userId); setUser(userData); }; loadUserData(); }, [userId]); if (!user) { return <Text>Loading...</Text>; } return ( <View> <Text>Name: {user.name}</Text> <Text>Email: {user.email}</Text> </View> ); }; export default UserProfile; // src/components/UserProfile.test.js import React from 'react'; import { render, screen, waitFor } from '@testing-library/react-native'; import UserProfile from './UserProfile'; import fetchUserData from '../api/userApi'; jest.mock('../api/userApi'); // Mock the entire module describe('UserProfile Component', () => { it('fetches and displays user data', async () => { const mockUserData = { id: 1, name: 'John Doe', email: 'john.doe@example.com' }; fetchUserData.mockResolvedValue(mockUserData); // Mock the return value render(<UserProfile userId={1} />); // Wait for the data to load and the component to update await waitFor(() => { expect(screen.getByText('Name: John Doe')).toBeDefined(); expect(screen.getByText('Email: john.doe@example.com')).toBeDefined(); }); }); it('displays loading state', () => { render(<UserProfile userId={1} />); expect(screen.getByText('Loading...')).toBeDefined(); }); it('handles error cases', async () => { fetchUserData.mockRejectedValue(new Error('Failed to fetch user data')); render(<UserProfile userId={1} />); await waitFor(() => { console.log(screen.debug()); // useful for debugging async render issues! //Note: error handling should be implemented in component, // in this test component should have error state and error message. //expect(screen.getByText('Error')).toBeDefined(); //This line is a placeholder! }); }); }); """ ### 2.5. Snapshot Testing **Do This:** * Use snapshot testing sparingly for components with complex or dynamic layouts. Snapshots are useful for detecting unintended UI changes. **Don't Do This:** * Rely solely on snapshot testing. Snapshots capture the UI's structure, but they don't verify functionality or behavior. * Commit snapshots without reviewing them. Ensure that any changes in the snapshot are intentional and correct. **Why:** * Snapshot tests quickly detect unexpected UI changes. However, they are less effective at verifying the component's logic. ## 3. Integration Testing ### 3.1. Scope **Do This:** * Integration tests should focus on verifying the interactions between two or more components or modules. * Test how your components function with Redux, Context, or other data management solutions **Don't Do This:** * Test entire user flows in integration tests. Reserve end-to-end (E2E) tests for that purpose. **Why:** * Integration tests ensure that different parts of the application work together correctly. ### 3.2. Tools & Libraries **Do This:** * Continue using Jest and React Native Testing Library. * Use mocking strategically to isolate parts of the integration. **Don't Do This:** * Avoid mocking unnecessarily. Aim to test as much of the real integration as possible. **Why:** * Minimal mocking reduces the risk of tests that are too tightly coupled to the implementation details. ### 3.3. Example: Integration Testing a List and Item Component """javascript // src/components/ListItem.js import React from 'react'; import { View, Text, TouchableOpacity } from 'react-native'; const ListItem = ({ item, onPress }) => { return ( <TouchableOpacity onPress={onPress}> <View> <Text>{item.name}</Text> </View> </TouchableOpacity> ); }; export default ListItem; // src/components/List.js import React from 'react'; import { FlatList } from 'react-native'; import ListItem from './ListItem'; const List = ({ items, onItemPress }) => { return ( <FlatList data={items} renderItem={({ item }) => ( <ListItem item={item} onPress={() => onItemPress(item)} /> )} keyExtractor={(item) => item.id.toString()} /> ); }; export default List; // src/components/List.test.js import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react-native'; import List from './List'; describe('List Component', () => { const items = [ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }, ]; const onItemPress = jest.fn(); it('renders a list of items', () => { render(<List items={items} onItemPress={onItemPress} />); expect(screen.getByText('Item 1')).toBeDefined(); expect(screen.getByText('Item 2')).toBeDefined(); }); it('calls onItemPress when an item is pressed', () => { render(<List items={items} onItemPress={onItemPress} />); const item1 = screen.getByText('Item 1'); fireEvent.press(item1); expect(onItemPress).toHaveBeenCalledWith(items[0]); }); }); """ ## 4. End-to-End (E2E) Testing ### 4.1. Scope **Do This:** * E2E tests should focus on verifying critical user flows through the entire application. * Simulate real user interactions as closely as possible. **Don't Do This:** * Test minor UI details. Focus on core functionality and user journeys. * Over-test the same flows repeatedly. Optimize for coverage of distinct scenarios. **Why:** * E2E tests provide the greatest confidence in the application's overall functionality. ### 4.2. Tools & Libraries **Do This:** * Consider using Detox or Appium for E2E testing. Detox is well-suited for React Native due to its grey-box testing approach that synchronizes with the app's state. **Don't Do This:** * Attempt to use unit testing frameworks for E2E testing. They are not designed for this purpose. **Why:** * Detox and Appium provide tools to automate UI testing and interact with native device features. ### 4.3. Example: E2E Test with Detox Detox requires specific setup and configuration as outlined in its documentation. The following is a simplified example. """javascript // e2e/firstTest.e2e.js describe('Example', () => { beforeAll(async () => { await device.launchApp(); }); beforeEach(async () => { await device.reloadReactNative(); }); it('should show hello screen after tap', async () => { await element(by.id('hello_button')).tap(); await expect(element(by.text('Hello!!!'))).toBeVisible(); }); it('should show world screen after tap', async () => { await element(by.id('world_button')).tap(); await expect(element(by.text('World!!!'))).toBeVisible(); }); }); """ ### 4.4. Test Data Management **Do This:** * Use a separate test database or API endpoint for E2E tests. This prevents tests from interfering with real user data. * Reset test data before each test suite. **Don't Do This:** * Use real user data in E2E tests. This is a security risk. **Why** * Ensures Tests are run in isolation. ### 4.5. Flakiness Reduction **Do This:** * Implement retry mechanisms for flaky tests. * Use explicit waits to ensure that UI elements are visible before interacting with them. * Write atomic E2E tests. **Don't Do This:** * Ignore flaky tests. They indicate underlying issues in the application or the test environment. **Why:** * Flaky tests undermine confidence in the test suite. By implementing retry mechanisms, explicit waits, and Atomic E2E tests reduces the likelihood of them. ## 5. Test Naming Conventions **Do This:** * Use descriptive and consistent test names. * Structure your test names to answer the following questions: What is being tested? Under what circumstances? What is the expected outcome? **Don't Do This:** * Use vague or ambiguous test names. **Why:** * Clear and concise test names make it easier to understand the purpose of each test and debug failures. """javascript // Example: // Good: it('fetches user data and displays it in the UI', () => { ... }); // Bad: it('test', () => { ... }); """ ## 6. Continuous Integration (CI) **Do This:** * Integrate your tests into your CI/CD pipeline to automatically run them on every commit or pull request. **Don't Do This:** * Skip running tests in CI. **Why:** * Continuous integration ensures that tests are executed regularly, providing early feedback on code changes. It helps to catch regression bugs before they reach production. ## 7. Accessibility Testing **Do This:** * Write accessibility tests to ensure that your application is usable by people with disabilities. * Use accessibility testing tools like React Native AccessibilityInfo or native accessibility APIs. **Don't Do This:** * Ignore accessibility. An accessible application is an inclusive application. **Why:** * Accessibility testing ensures that your application is inclusive and adheres to accessibility standards like WCAG. ## 8. Performance Testing ### 8.1. Scope **Do This:** * Measure rendering performance using tools like "PerfMonitor". * Perform stress testing for key components. **Don't Do This:** * Ignore performance testing until late in the development cycle. **Why:** * Improves application performance ### 8.2. Tools & Libraries **Do This:** * Use tools like Flipper for profiling. * Use Jest for performance testing. """javascript //Performance test example // src/components/List.performance.test.js import List from './List'; import React from 'react'; import { render } from '@testing-library/react-native'; describe('List Component - Performance', () => { it('renders 1000 items without performance degradation', () => { const items = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: "Item ${i}" })); const startTime = performance.now(); render(<List items={items} onItemPress={() => {}} />); const endTime = performance.now(); const renderTime = endTime - startTime; console.log("Rendering 1000 items took ${renderTime} ms"); expect(renderTime).toBeLessThan(500); // Adjust the threshold as needed //If it's consistently taking over 500ms to render, your app might feel sluggish }); }); """ ## 9. Security Testing ### 9.1. Scope **Do This:** * Implement security tests to catch common vulnerabilities, such as data injection attacks. * Use static analysis tools to identify potential vulnerabilities in your code. **Don't Do This:** * Assume that your application is secure by default. * Store sensitive information, such as API keys, directly in your code. **Why:** * Improves the security of your application. ## 10. Deprecated Features * Double check your version of React Native and ensure that you are not calling any deprecated features. By following the testing methodology standards in your React Native projects, you ensure higher code quality, improve maintainability, and deliver a more reliable and secure user experience. Adapt and extend these standards to match the specific needs of your projects and organization.