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
All features must work on iOS , Android , Web , mWeb (mobile web), and macOS .
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' ),
})();
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 >
);
}
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 );
} ,
} ;
// 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 ;
Navigation
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
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