Skip to main content

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

Build docs developers (and LLMs) love