Skip to main content

Overview

New Expensify uses React Native to deliver a native experience on iOS, Android, and Web from a single codebase. This guide covers the React Native-specific aspects of the architecture.

React Native Version

The app uses the latest stable version of React Native with the New Architecture enabled:
  • Fabric Renderer: New rendering system
  • TurboModules: Improved native module integration
  • Hermes: JavaScript engine optimized for React Native

Platform Support

All features must work on iOS, Android, Web, mWeb (mobile web), and macOS.

Platform-Specific Code

When platform-specific code is necessary, use the Platform module:
import {Platform} from 'react-native';

// Platform checks
if (Platform.OS === 'ios') {
  // iOS-specific code
}

// Platform.select for values
const padding = Platform.select({
  ios: 10,
  android: 12,
  web: 16,
  default: 10,
});

// Platform.select for components
const Component = Platform.select({
  ios: () => require('./ComponentIOS'),
  android: () => require('./ComponentAndroid'),
  default: () => require('./ComponentDefault'),
})();

File Extensions for Platform-Specific Files

Component.tsx          # Shared implementation
Component.ios.tsx      # iOS-specific
Component.android.tsx  # Android-specific
Component.web.tsx      # Web-specific
Component.native.tsx   # iOS + Android (not web)
React Native automatically loads the correct file based on the platform.

Component Structure

Base Components

New Expensify has custom wrappers for many React Native primitives in src/components/:
// Use custom components instead of React Native primitives
import Text from '@components/Text';           // Not from 'react-native'
import View from '@components/View';
import Button from '@components/Button';
import TextInput from '@components/TextInput';
Why custom components?
  • Consistent styling across the app
  • Accessibility built-in
  • Performance optimizations
  • Type safety

Screen Wrapper

All page components should use ScreenWrapper:
import ScreenWrapper from '@components/ScreenWrapper';
import HeaderWithBackButton from '@components/HeaderWithBackButton';

function MyScreen() {
  return (
    <ScreenWrapper testID="MyScreen">
      <HeaderWithBackButton title="My Screen" />
      {/* Screen content */}
    </ScreenWrapper>
  );
}
ScreenWrapper provides:
  • Safe area handling
  • Keyboard avoiding behavior
  • Scroll view when needed
  • Loading states
  • Offline indicators

Styling

Theme System

All styles use the centralized theme:
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={{color: theme.text}}>Hello</Text>
    </View>
  );
}

StyleSheet

Always use StyleSheet.create() for performance:
import {StyleSheet} from 'react-native';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
  },
  text: {
    fontSize: 16,
    fontWeight: '600',
  },
});

Responsive Design

Use the responsive layout hook:
import useResponsiveLayout from '@hooks/useResponsiveLayout';

function MyComponent() {
  const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();

  return (
    <View style={{flexDirection: shouldUseNarrowLayout ? 'column' : 'row'}}>
      {/* Content */}
    </View>
  );
}

Performance Best Practices

1. Memoization

import React, {memo, useMemo, useCallback} from 'react';

// Memoize components
const ExpensiveComponent = memo(function ExpensiveComponent({data}) {
  return <View>{/* Render data */}</View>;
});

// Memoize values
function MyComponent({items}) {
  const sortedItems = useMemo(
    () => items.sort((a, b) => a.name.localeCompare(b.name)),
    [items],
  );

  const handlePress = useCallback(() => {
    console.log('Pressed');
  }, []);

  return <ExpensiveComponent data={sortedItems} onPress={handlePress} />;
}

2. FlashList for Long Lists

Use FlashList instead of FlatList for better performance:
import {FlashList} from '@shopify/flash-list';

function MyList({data}) {
  return (
    <FlashList
      data={data}
      renderItem={({item}) => <ListItem item={item} />}
      estimatedItemSize={100}
    />
  );
}

3. Avoid Inline Functions and Styles

// ❌ Bad - creates new function on every render
<Button onPress={() => console.log('pressed')} />

// ✅ Good - uses memoized callback
const handlePress = useCallback(() => console.log('pressed'), []);
<Button onPress={handlePress} />

// ❌ Bad - creates new style object on every render
<View style={{padding: 16}} />

// ✅ Good - uses StyleSheet
<View style={styles.container} />

Native Modules

Using Native Modules

When React Native APIs aren’t sufficient, use native modules:
// src/libs/SomeNativeModule/index.ts
import {NativeModules} from 'react-native';

const {SomeNativeModule} = NativeModules;

export default {
  doSomething: (param: string): Promise<string> => {
    return SomeNativeModule.doSomething(param);
  },
};

Platform-Specific Modules

// index.ts - exports the interface
export type SomeModule = {
  doSomething: (param: string) => Promise<string>;
};

// index.native.ts - iOS/Android implementation
import {NativeModules} from 'react-native';
import type {SomeModule} from './index';

const {SomeNativeModule} = NativeModules;

export default {
  doSomething: SomeNativeModule.doSomething,
} as SomeModule;

// index.ts or index.web.ts - Web implementation
import type {SomeModule} from './index';

export default {
  doSomething: async (param: string) => {
    // Web-specific implementation
    return `Web result: ${param}`;
  },
} as SomeModule;
New Expensify uses React Navigation. See Navigation System for details.

Basic Navigation Example

import Navigation from '@libs/Navigation/Navigation';
import ROUTES from '@src/ROUTES';

function MyComponent() {
  const handleNavigate = () => {
    Navigation.navigate(ROUTES.SETTINGS);
  };

  const handleGoBack = () => {
    Navigation.goBack();
  };

  return (
    <View>
      <Button onPress={handleNavigate}>Go to Settings</Button>
      <Button onPress={handleGoBack}>Go Back</Button>
    </View>
  );
}

Accessibility

All components must be accessible:
function AccessibleComponent() {
  return (
    <View
      accessible
      accessibilityLabel="Main container"
      accessibilityRole="region"
    >
      <Button
        accessibilityLabel="Submit form"
        accessibilityHint="Submits the expense report"
        onPress={handleSubmit}
      >
        Submit
      </Button>
    </View>
  );
}

Accessibility Properties

  • accessible: Groups children for screen readers
  • accessibilityLabel: Description read by screen readers
  • accessibilityHint: Additional context
  • accessibilityRole: Semantic role (button, link, header, etc.)
  • accessibilityState: Current state (disabled, selected, etc.)

Internationalization (i18n)

All user-facing strings must be translated:
import {useLocalize} from '@hooks/useLocalize';

function MyComponent() {
  const {translate} = useLocalize();

  return (
    <View>
      <Text>{translate('common.submit')}</Text>
      <Text>{translate('workspace.editor.nameInputLabel')}</Text>
    </View>
  );
}
Translation files are in src/languages/:
  • en.ts - English
  • es.ts - Spanish
  • etc.

Image Handling

Static Images

import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';

function MyComponent() {
  return <Icon src={Expensicons.Plus} />;
}

Remote Images

import Image from '@components/Image';

function MyComponent() {
  return (
    <Image
      source={{uri: 'https://example.com/image.png'}}
      style={{width: 100, height: 100}}
      resizeMode="cover"
    />
  );
}

Keyboard Handling

import {Keyboard} from 'react-native';
import {useKeyboardState} from '@hooks/useKeyboardState';

function MyComponent() {
  const {isKeyboardShown} = useKeyboardState();

  const handleSubmit = () => {
    Keyboard.dismiss();
    // Process form
  };

  return (
    <View>
      {!isKeyboardShown && <Footer />}
    </View>
  );
}

Testing React Native Components

import {render, fireEvent} from '@testing-library/react-native';
import MyComponent from '../MyComponent';

describe('MyComponent', () => {
  it('renders correctly', () => {
    const {getByText} = render(<MyComponent />);
    expect(getByText('Hello')).toBeTruthy();
  });

  it('handles press events', () => {
    const onPress = jest.fn();
    const {getByText} = render(<MyComponent onPress={onPress} />);
    
    fireEvent.press(getByText('Submit'));
    expect(onPress).toHaveBeenCalled();
  });
});

Common Pitfalls

1. Not Testing on All Platforms

Always test your changes on iOS, Android, Web, and mobile web before submitting a PR.

2. Hardcoded Colors/Sizes

// ❌ Bad
<View style={{backgroundColor: '#000000', padding: 16}} />

// ✅ Good
const theme = useTheme();
const styles = useThemeStyles();
<View style={[styles.container, {backgroundColor: theme.appBG}]} />

3. Missing Keys in Lists

// ❌ Bad
{items.map(item => <Item item={item} />)}

// ✅ Good
{items.map(item => <Item key={item.id} item={item} />)}

4. Not Handling Offline State

See Offline-First Architecture for proper offline handling.

Device-Specific Features

Camera Access

import * as ImagePicker from 'react-native-image-picker';

const takePhoto = async () => {
  const result = await ImagePicker.launchCamera({
    mediaType: 'photo',
    quality: 0.8,
  });
  
  if (result.assets?.[0]) {
    // Process photo
  }
};

Location Services

import Geolocation from '@react-native-community/geolocation';

Geolocation.getCurrentPosition(
  (position) => {
    console.log(position.coords);
  },
  (error) => console.error(error),
  {enableHighAccuracy: true},
);

Push Notifications

Push notifications are handled via Urban Airship (Airship):
// Integration is in native code
// See src/libs/Notifications/ for the JavaScript interface

Debugging

React Native Debugger

# Enable debug mode
# iOS: Cmd+D in simulator
# Android: Cmd+M in emulator

Flipper

Flipper is enabled for React Native debugging:
  • Network inspector
  • Layout inspector
  • Redux/Onyx state
  • Performance metrics

Build Commands

# iOS
npm run ios

# Android
npm run android

# Web
npm run web

# macOS
npm run macos

Next Steps

State Management

Learn about Onyx state management

Navigation

Understand navigation patterns

Testing

Write tests for React Native components

Best Practices

Follow React Native best practices

Build docs developers (and LLMs) love