Overview
New Expensify follows strict coding standards to maintain code quality and consistency across the codebase.Read the complete STYLE.md guide in the repository for comprehensive style guidelines.
Code Quality Tools
1. TypeScript
All code must be written in TypeScript with strict mode enabled.// ✅ Good: Properly typed
function calculateTotal(amounts: number[]): number {
return amounts.reduce((sum, amount) => sum + amount, 0);
}
// ❌ Bad: Using 'any'
function calculateTotal(amounts: any): any {
return amounts.reduce((sum: any, amount: any) => sum + amount, 0);
}
2. ESLint
All code must pass ESLint with zero warnings.# Run linter
npm run lint
# Auto-fix issues
npm run lint -- --fix
3. Prettier
MANDATORY: All code must be formatted with Prettier before committing.# Format all files
npm run prettier
# Format specific files
npx prettier --write src/pages/MyPage.tsx
CI will reject PRs with improperly formatted code. Always run Prettier before committing.
Post-Edit Checklist
Run these commands before every commit:# 1. Format code (REQUIRED)
npm run prettier
# 2. Check linting
npm run lint
# 3. Type check (if you changed types/interfaces)
npm run typecheck-tsgo
TypeScript Guidelines
Type Definitions
// ✅ Good: Explicit types
interface ReportAction {
reportActionID: string;
message: string;
created: string;
person: PersonalDetails[];
}
function processAction(action: ReportAction): void {
console.log(action.message);
}
// ❌ Bad: Implicit 'any'
function processAction(action) {
console.log(action.message);
}
Avoid Type Assertions
// ❌ Bad: Type assertion
const value = someValue as string;
// ✅ Good: Type guard
function isString(value: unknown): value is string {
return typeof value === 'string';
}
if (isString(someValue)) {
// TypeScript knows someValue is string here
}
Use Utility Types
import type {ValueOf} from 'type-fest';
import type CONST from '@src/CONST';
// Pick specific fields
type ReportSummary = Pick<Report, 'reportID' | 'reportName'>;
// Make fields optional
type PartialReport = Partial<Report>;
// Extract values from const
type PendingAction = ValueOf<typeof CONST.RED_BRICK_ROAD_PENDING_ACTION>;
Naming Conventions
Files
components/Button.tsx # PascalCase for components
libs/actions/Report.ts # PascalCase for modules
utils/getReportName.ts # camelCase for utilities
types/onyx/Report.ts # PascalCase for types
Variables and Functions
// ✅ Good: Descriptive names
const reportActionID = '123';
const isReportLoading = false;
function createOptimisticReportAction(message: string): ReportAction {
// ...
}
// ❌ Bad: Abbreviated or unclear names
const raID = '123';
const loading = false;
function createORA(msg: string) {
// ...
}
Components
// ✅ Good: PascalCase, descriptive
function ReportActionsList({actions}: ReportActionsListProps) {
return <View>...</View>;
}
// ❌ Bad: lowercase or generic
function list({actions}) {
return <View>...</View>;
}
Constants
// ✅ Good: SCREAMING_SNAKE_CASE in CONST.ts
const MAX_RETRY_ATTEMPTS = 3;
const DEFAULT_CURRENCY = 'USD';
// Local constants can be camelCase
const defaultProps = {visible: true};
Component Patterns
Functional Components with Types
import type {ReactNode} from 'react';
type MyComponentProps = {
title: string;
isVisible?: boolean;
children?: ReactNode;
onPress: () => void;
};
function MyComponent({
title,
isVisible = true,
children,
onPress,
}: MyComponentProps) {
if (!isVisible) {
return null;
}
return (
<View>
<Text>{title}</Text>
{children}
<Button onPress={onPress}>Submit</Button>
</View>
);
}
export default MyComponent;
Memoization
import {memo, useMemo, useCallback} from 'react';
// Memoize components
const ExpensiveComponent = memo(function ExpensiveComponent({
data,
}: ExpensiveComponentProps) {
return <View>{/* Render data */}</View>;
});
function ParentComponent({items}: ParentComponentProps) {
// Memoize computed values
const sortedItems = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items],
);
// Memoize callbacks
const handlePress = useCallback(() => {
console.log('Pressed');
}, []);
return <ExpensiveComponent data={sortedItems} onPress={handlePress} />;
}
Props Destructuring
// ✅ Good: Destructure in function signature
function MyComponent({title, onPress}: MyComponentProps) {
return <Button title={title} onPress={onPress} />;
}
// ❌ Bad: Use props object
function MyComponent(props: MyComponentProps) {
return <Button title={props.title} onPress={props.onPress} />;
}
Styling Guidelines
Use Theme and Styles Hooks
import {useTheme} from '@hooks/useTheme';
import {useThemeStyles} from '@hooks/useThemeStyles';
function MyComponent() {
const theme = useTheme();
const styles = useThemeStyles();
return (
<View style={styles.container}>
<Text style={[styles.label, {color: theme.text}]}>
Hello
</Text>
</View>
);
}
StyleSheet
import {StyleSheet} from 'react-native';
// ✅ Good: Use StyleSheet.create
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
text: {
fontSize: 16,
fontWeight: '600',
},
});
// ❌ Bad: Inline styles
<View style={{flex: 1, padding: 16}} />
Combining Styles
// ✅ Good: Array of styles
<View style={[styles.container, styles.highlighted]} />
// Conditional styles
<View
style={[
styles.container,
isHighlighted && styles.highlighted,
isDisabled && styles.disabled,
]}
/>
Import Organization
// 1. React imports
import {useState, useEffect, useCallback} from 'react';
import {View, Text} from 'react-native';
// 2. Third-party imports
import {useOnyx} from 'react-native-onyx';
// 3. Component imports
import Button from '@components/Button';
import ScreenWrapper from '@components/ScreenWrapper';
// 4. Hook imports
import {useLocalize} from '@hooks/useLocalize';
import {useThemeStyles} from '@hooks/useThemeStyles';
// 5. Utility imports
import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
// 6. Type imports
import type {Report} from '@src/types/onyx';
import type {ReportActionListProps} from './types';
// 7. Constant imports
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
React Patterns
Hooks
// ✅ Good: Custom hooks start with 'use'
function useReportActions(reportID: string) {
const [actions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`);
const sortedActions = useMemo(
() => Object.values(actions ?? {}).sort((a, b) =>
new Date(a.created).getTime() - new Date(b.created).getTime()
),
[actions],
);
return sortedActions;
}
// Usage
function ReportScreen({reportID}: ReportScreenProps) {
const actions = useReportActions(reportID);
return <ActionsList actions={actions} />;
}
Conditional Rendering
// ✅ Good: Early return
function MyComponent({isVisible, data}: MyComponentProps) {
if (!isVisible) {
return null;
}
return <View>{data}</View>;
}
// ✅ Good: Ternary for simple cases
function MyComponent({isLoading, data}: MyComponentProps) {
return isLoading ? <LoadingSpinner /> : <DataView data={data} />;
}
// ❌ Bad: Nested ternaries
function MyComponent({isLoading, hasError, data}: MyComponentProps) {
return isLoading ? (
<LoadingSpinner />
) : hasError ? (
<ErrorView />
) : (
<DataView data={data} />
);
}
Lists and Keys
// ✅ Good: Unique, stable keys
function ItemList({items}: ItemListProps) {
return (
<View>
{items.map((item) => (
<Item key={item.id} item={item} />
))}
</View>
);
}
// ❌ Bad: Index as key
function ItemList({items}: ItemListProps) {
return (
<View>
{items.map((item, index) => (
<Item key={index} item={item} />
))}
</View>
);
}
Performance Best Practices
Avoid Inline Functions
// ❌ Bad: Creates new function on each render
function MyComponent() {
return <Button onPress={() => console.log('pressed')} />;
}
// ✅ Good: Memoized callback
function MyComponent() {
const handlePress = useCallback(() => {
console.log('pressed');
}, []);
return <Button onPress={handlePress} />;
}
Use FlashList for Long Lists
import {FlashList} from '@shopify/flash-list';
// ✅ Good: FlashList for better performance
function ReportList({reports}: ReportListProps) {
return (
<FlashList
data={reports}
renderItem={({item}) => <ReportItem report={item} />}
estimatedItemSize={100}
/>
);
}
Avoid Unnecessary Re-renders
// ✅ Good: Memoized component
const ReportItem = memo(function ReportItem({report}: ReportItemProps) {
return <View>{report.reportName}</View>;
});
// ✅ Good: Selector to prevent re-renders
function MyComponent({reportID}: MyComponentProps) {
const [reportName] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {
selector: (report) => report?.reportName,
});
return <Text>{reportName}</Text>;
}
Error Handling
Try-Catch Blocks
// ✅ Good: Proper error handling
async function loadReportData(reportID: string) {
try {
const data = await fetchReportData(reportID);
processReportData(data);
} catch (error) {
Log.error('[Report] Failed to load report', {reportID, error});
showErrorMessage('Failed to load report');
}
}
Logging
import Log from '@libs/Log';
// ✅ Good: Use Log utility
Log.info('[Report] Opening report', {reportID});
Log.warn('[Report] Report has errors', {reportID, errors});
Log.error('[Report] Failed to save', {reportID, error});
// ❌ Bad: console.log
console.log('Opening report', reportID);
Comments
When to Comment
// ✅ Good: Explain WHY, not WHAT
// We need to wait for the animation to complete before navigating
// otherwise the screen transition looks janky
setTimeout(() => {
Navigation.navigate(ROUTES.HOME);
}, 300);
// ❌ Bad: Obvious comments
// Set loading to true
setIsLoading(true);
JSDoc for Complex Functions
/**
* Calculates the total amount of all transactions in a report.
* Excludes transactions that are deleted or have errors.
*
* @param reportID - The ID of the report
* @returns The total amount in cents
*/
function calculateReportTotal(reportID: string): number {
// Implementation
}
Testing Standards
See Testing Guidelines for comprehensive testing standards.Basic Test Structure
import {render} from '@testing-library/react-native';
import MyComponent from '../MyComponent';
describe('MyComponent', () => {
it('renders correctly', () => {
const {getByText} = render(<MyComponent title="Test" />);
expect(getByText('Test')).toBeTruthy();
});
it('calls onPress when button is pressed', () => {
const onPress = jest.fn();
const {getByText} = render(
<MyComponent title="Test" onPress={onPress} />
);
fireEvent.press(getByText('Submit'));
expect(onPress).toHaveBeenCalled();
});
});
Git Commit Standards
Commit Messages
# ✅ Good: Clear, descriptive
git commit -m "Fix: Prevent crash when opening empty report"
git commit -m "Add: Support for multi-currency reports"
git commit -m "Update: Improve report loading performance"
# ❌ Bad: Vague or unclear
git commit -m "fix bug"
git commit -m "updates"
git commit -m "wip"
Commit Signing
All commits must be signed:# Configure GPG signing
git config --global user.signingkey YOUR_KEY_ID
git config --global commit.gpgsign true
Common Anti-Patterns
1. Mutating State
// ❌ Bad: Mutating state
const items = [...state.items];
items.push(newItem);
setState({items});
// ✅ Good: Immutable update
setState({items: [...state.items, newItem]});
2. Side Effects in Render
// ❌ Bad: Side effect in render
function MyComponent() {
API.write('LogView', {}); // Side effect!
return <View />;
}
// ✅ Good: Side effect in useEffect
function MyComponent() {
useEffect(() => {
API.write('LogView', {});
}, []);
return <View />;
}
3. Missing Dependencies
// ❌ Bad: Missing dependencies
useEffect(() => {
processData(data);
}, []); // data should be in dependency array
// ✅ Good: All dependencies included
useEffect(() => {
processData(data);
}, [data]);
Platform-Specific Code
import {Platform} from 'react-native';
// ✅ Good: Platform.select
const padding = Platform.select({
ios: 10,
android: 12,
web: 16,
default: 10,
});
// ✅ Good: Platform-specific files
// Component.tsx (shared)
// Component.ios.tsx (iOS-specific)
// Component.android.tsx (Android-specific)
// Component.web.tsx (Web-specific)
Accessibility
// ✅ Good: Accessible component
function AccessibleButton({label, onPress}: AccessibleButtonProps) {
return (
<Pressable
accessible
accessibilityRole="button"
accessibilityLabel={label}
onPress={onPress}
>
<Text>{label}</Text>
</Pressable>
);
}
Internationalization
import {useLocalize} from '@hooks/useLocalize';
// ✅ Good: Translated strings
function MyComponent() {
const {translate} = useLocalize();
return (
<View>
<Text>{translate('common.submit')}</Text>
<Text>{translate('workspace.editor.nameInputLabel')}</Text>
</View>
);
}
// ❌ Bad: Hardcoded strings
function MyComponent() {
return (
<View>
<Text>Submit</Text>
<Text>Workspace Name</Text>
</View>
);
}
Documentation
- Complex logic: Add comments explaining WHY
- Public APIs: Use JSDoc
- New patterns: Document in CONTRIBUTING.md
- Breaking changes: Update relevant docs
Next Steps
Testing
Learn testing best practices
Pull Requests
Submit quality PRs
Architecture
Understand the codebase structure
Style Guide
Complete style guide
