Documentation Index Fetch the complete documentation index at: https://mintlify.com/Expensify/App/llms.txt
Use this file to discover all available pages before exploring further.
Introduction
New Expensify uses React Native Onyx , a custom state management library built specifically for offline-first applications. Onyx provides automatic persistence, optimistic updates, and conflict resolution out of the box.
Why Onyx?
Traditional state management solutions like Redux or MobX weren’t designed for offline-first apps. Onyx solves unique challenges:
Automatic Persistence : All data is automatically saved to disk
Optimistic Updates : UI updates immediately, syncs later
Conflict Resolution : Built-in strategies for handling sync conflicts
Performance : Batched updates and selective re-renders
Type Safety : Full TypeScript support
Core Concepts
1. Onyx Keys
All data in Onyx is stored with unique keys defined in src/ONYXKEYS.ts:
const ONYXKEYS = {
// Session data
SESSION: 'session' ,
ACCOUNT: 'account' ,
// User data
PERSONAL_DETAILS_LIST: 'personalDetailsList' ,
// Network state
NETWORK: 'network' ,
// Collections (multi-key storage)
COLLECTION: {
REPORT: 'report_' , // report_123, report_456, etc.
TRANSACTION: 'transactions_' ,
POLICY: 'policy_' ,
},
} as const ;
2. Onyx Methods
Reading Data with connect
import Onyx from 'react-native-onyx' ;
import ONYXKEYS from '@src/ONYXKEYS' ;
// Subscribe to changes
const connectionID = Onyx . connect ({
key: ONYXKEYS . SESSION ,
callback : ( session ) => {
console . log ( 'Session updated:' , session );
},
});
// Clean up when done
Onyx . disconnect ( connectionID );
Writing Data
// Merge: Update specific fields
Onyx . merge ( ONYXKEYS . SESSION , {
authToken: 'new-token' ,
accountID: 12345 ,
});
// Set: Replace entire value
Onyx . set ( ONYXKEYS . SESSION , {
authToken: 'new-token' ,
accountID: 12345 ,
email: 'user@example.com' ,
});
// Multi-set: Update multiple keys at once
Onyx . mergeCollection ( ONYXKEYS . COLLECTION . REPORT , {
report_123: { reportName: 'Updated Name' },
report_456: { reportName: 'Another Update' },
});
3. Using Onyx with React
Use the useOnyx hook (recommended):
import { useOnyx } from 'react-native-onyx' ;
import ONYXKEYS from '@src/ONYXKEYS' ;
function MyComponent () {
const [ session ] = useOnyx ( ONYXKEYS . SESSION );
const [ network ] = useOnyx ( ONYXKEYS . NETWORK );
if ( ! session ) {
return < LoadingScreen />;
}
return (
< View >
< Text > Account ID : { session . accountID }</ Text >
< Text > Network : { network ?. isOffline ? 'Offline' : 'Online' }</ Text >
</ View >
);
}
With selectors for performance:
function MyComponent () {
const [ accountID ] = useOnyx ( ONYXKEYS . SESSION , {
selector : ( session ) => session ?. accountID ,
});
return < Text > Account ID : { accountID } </ Text > ;
}
4. Collection Keys
Collections store multiple related items:
// Read a specific report
const [ report ] = useOnyx ( ` ${ ONYXKEYS . COLLECTION . REPORT }${ reportID } ` );
// Read all reports
const [ allReports ] = useOnyx ( ONYXKEYS . COLLECTION . REPORT , {
allowStaleData: true ,
});
// Update a specific report
Onyx . merge ( ` ${ ONYXKEYS . COLLECTION . REPORT }${ reportID } ` , {
reportName: 'New Name' ,
});
Onyx Updates in API Calls
API calls use Onyx updates to manage state:
import API from '@libs/API' ;
import type { OnyxUpdate } from '@src/types/onyx/Request' ;
function createWorkspace ( policyName : string ) {
const policyID = generateRandomID ();
// Optimistic data: applied immediately
const optimisticData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . SET ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ,
value: {
id: policyID ,
name: policyName ,
type: CONST . POLICY . TYPE . TEAM ,
pendingAction: CONST . RED_BRICK_ROAD_PENDING_ACTION . ADD ,
},
},
];
// Success data: applied when API succeeds
const successData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ,
value: {
pendingAction: null ,
},
},
];
// Failure data: applied when API fails
const failureData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . SET ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ,
value: null , // Remove the optimistic policy
},
];
API . write ( 'CreateWorkspace' , { policyName }, {
optimisticData ,
successData ,
failureData ,
});
}
Onyx Method Types
Method Description Example SETReplace entire value Reset a policy MERGEUpdate specific fields Update policy name MERGE_COLLECTIONUpdate multiple collection items Bulk update reports
ONYXKEYS Reference
From src/ONYXKEYS.ts:
Authentication & Session
ONYXKEYS . SESSION // Current user session
ONYXKEYS . ACCOUNT // User account info
ONYXKEYS . CREDENTIALS // Stored credentials
ONYXKEYS . STASHED_SESSION // Temporary session storage
User Data
ONYXKEYS . PERSONAL_DETAILS_LIST // All user profiles
ONYXKEYS . PRIVATE_PERSONAL_DETAILS // Current user's private data
ONYXKEYS . LOGIN_LIST // User's login methods
Reports & Messages
ONYXKEYS . COLLECTION . REPORT // report_<reportID>
ONYXKEYS . COLLECTION . REPORT_ACTIONS // reportActions_<reportID>
ONYXKEYS . COLLECTION . REPORT_DRAFT // reportDraft_<reportID>
ONYXKEYS . COLLECTION . REPORT_METADATA // reportMetadata_<reportID>
Transactions
ONYXKEYS . COLLECTION . TRANSACTION // transactions_<transactionID>
ONYXKEYS . COLLECTION . TRANSACTION_DRAFT // transactionsDraft_<transactionID>
Policies (Workspaces)
ONYXKEYS . COLLECTION . POLICY // policy_<policyID>
ONYXKEYS . COLLECTION . POLICY_CATEGORIES // policyCategories_<policyID>
ONYXKEYS . COLLECTION . POLICY_TAGS // policyTags_<policyID>
Network & App State
ONYXKEYS . NETWORK // Network connectivity status
ONYXKEYS . IS_LOADING_APP // App loading state
ONYXKEYS . CURRENT_DATE // Current date (for consistency)
1. Use Selectors
Selectors prevent unnecessary re-renders:
// ❌ Bad: Component re-renders when ANY session field changes
const [ session ] = useOnyx ( ONYXKEYS . SESSION );
return < Text >{session?. email } </ Text > ;
// ✅ Good: Component only re-renders when email changes
const [ email ] = useOnyx ( ONYXKEYS . SESSION , {
selector : ( session ) => session ?. email ,
});
return < Text >{ email } </ Text > ;
2. Allow Stale Data
For performance-critical components:
const [ reports ] = useOnyx ( ONYXKEYS . COLLECTION . REPORT , {
allowStaleData: true ,
});
3. Batch Updates
Onyx automatically batches updates, but you can optimize further:
// Multiple updates are automatically batched
Onyx . merge ( ONYXKEYS . SESSION , { authToken: 'token1' });
Onyx . merge ( ONYXKEYS . ACCOUNT , { validated: true });
Onyx . merge ( ONYXKEYS . NETWORK , { isOffline: false });
// All three updates trigger a single re-render
Separate loading states from data to prevent re-renders:
// Data and metadata are in separate keys
ONYXKEYS . COLLECTION . REPORT // Actual report data
ONYXKEYS . COLLECTION . REPORT_METADATA // Loading states, errors
// Component only subscribes to what it needs
function ReportView ({ reportID }) {
const [ report ] = useOnyx ( ` ${ ONYXKEYS . COLLECTION . REPORT }${ reportID } ` );
// Doesn't re-render when loading state changes
return < ReportContent report ={ report } />;
}
function ReportLoadingIndicator ({ reportID }) {
const [ metadata ] = useOnyx (
` ${ ONYXKEYS . COLLECTION . REPORT_METADATA }${ reportID } ` ,
);
// Doesn't re-render when report data changes
return metadata ?. isLoading ? < LoadingSpinner /> : null ;
}
Offline Behavior
Pending Actions
Use pendingAction to indicate data state:
type PendingAction = 'add' | 'update' | 'delete' ;
const optimisticData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . REPORT }${ reportID } ` ,
value: {
reportName: newName ,
pendingAction: CONST . RED_BRICK_ROAD_PENDING_ACTION . UPDATE ,
},
},
];
UI components can then show pending state:
import OfflineWithFeedback from '@components/OfflineWithFeedback' ;
function ReportItem ({ report }) {
return (
< OfflineWithFeedback pendingAction = {report. pendingAction } >
< MenuItem title = {report. reportName } />
</ OfflineWithFeedback >
);
}
Error Handling
Store errors in Onyx:
const failureData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ,
value: {
errors: {
[Date. now ()]: 'Failed to create workspace' ,
},
pendingAction: null ,
},
},
];
Display errors with OfflineWithFeedback:
function WorkspaceItem ({ policy }) {
const handleDismissError = () => {
Onyx . merge ( ` ${ ONYXKEYS . COLLECTION . POLICY }${ policy . id } ` , {
errors: null ,
});
};
return (
< OfflineWithFeedback
errors = {policy. errors }
onClose = { handleDismissError }
>
< MenuItem title = {policy. name } />
</ OfflineWithFeedback >
);
}
Testing with Onyx
Setup
import Onyx from 'react-native-onyx' ;
import ONYXKEYS from '@src/ONYXKEYS' ;
beforeAll (() => {
Onyx . init ({
keys: ONYXKEYS ,
safeEvictionKeys: [ ONYXKEYS . COLLECTION . REPORT ],
});
});
afterEach (() => {
Onyx . clear ();
});
Testing Components
import { render , waitFor } from '@testing-library/react-native' ;
import Onyx from 'react-native-onyx' ;
import ONYXKEYS from '@src/ONYXKEYS' ;
test ( 'displays session email' , async () => {
await Onyx . merge ( ONYXKEYS . SESSION , {
email: 'test@example.com' ,
});
const { getByText } = render (< MyComponent />);
await waitFor (() => {
expect ( getByText ( 'test@example.com' )). toBeTruthy ();
});
});
Testing Actions
import waitForBatchedUpdates from '@libs/waitForBatchedUpdates' ;
test ( 'creates workspace optimistically' , async () => {
createWorkspace ( 'Test Workspace' );
await waitForBatchedUpdates ();
const connection = Onyx . connect ({
key: ONYXKEYS . COLLECTION . POLICY ,
callback : ( policies ) => {
const policy = Object . values ( policies )[ 0 ];
expect ( policy . name ). toBe ( 'Test Workspace' );
expect ( policy . pendingAction ). toBe ( 'add' );
},
});
Onyx . disconnect ( connection );
});
Best Practices
1. Always Use Typed Keys
// ❌ Bad: Magic strings
Onyx . merge ( 'session' , { authToken: 'token' });
// ✅ Good: Use ONYXKEYS constants
Onyx . merge ( ONYXKEYS . SESSION , { authToken: 'token' });
2. Clean Up Connections
useEffect (() => {
const connectionID = Onyx . connect ({
key: ONYXKEYS . SESSION ,
callback: handleSessionChange ,
});
return () => Onyx . disconnect ( connectionID );
}, []);
3. Use Appropriate Merge vs Set
// Use MERGE to update fields
Onyx . merge ( ONYXKEYS . SESSION , { authToken: 'new-token' });
// Other fields remain unchanged
// Use SET to replace entirely
Onyx . set ( ONYXKEYS . SESSION , {
authToken: 'new-token' ,
accountID: 12345 ,
email: 'user@example.com' ,
});
// All previous fields are removed
4. Provide All Three Data Sets
For write operations, always include optimistic, success, and failure data:
API . write ( 'SomeCommand' , parameters , {
optimisticData , // Shows immediately
successData , // Applied on success
failureData , // Applied on failure
});
5. Use finallyData When Appropriate
If success and failure data are identical:
const finallyData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ONYXKEYS . IS_LOADING ,
value: false ,
},
];
API . write ( 'SomeCommand' , parameters , {
optimisticData ,
finallyData , // Applied regardless of success/failure
});
Advanced Patterns
Derived Data
Compute derived values outside Onyx:
function useReportTotal ( reportID : string ) {
const [ transactions ] = useOnyx ( ONYXKEYS . COLLECTION . TRANSACTION );
return useMemo (() => {
const reportTransactions = Object . values ( transactions ). filter (
t => t . reportID === reportID ,
);
return reportTransactions . reduce (( sum , t ) => sum + t . amount , 0 );
}, [ transactions , reportID ]);
}
Conditional Subscriptions
function useConditionalData ( shouldFetch : boolean ) {
const [ data ] = useOnyx (
shouldFetch ? ONYXKEYS . SOME_DATA : null ,
);
return data ;
}
Debugging Onyx
Enable in .env:
Then view Onyx state in Redux DevTools browser extension.
Logging Onyx Changes
Onyx . connect ({
key: ONYXKEYS . SESSION ,
callback : ( value , key ) => {
console . log ( `Onyx ${ key } changed:` , value );
},
});
Troubleshooting
Data Not Updating
Check that you’re using the correct Onyx key
Verify optimisticData/successData/failureData are set correctly
Check network tab for API responses
Look for errors in failureData being applied
Use selectors to prevent unnecessary re-renders
Use allowStaleData for non-critical data
Separate data and metadata into different keys
Check for missing React.memo on components
Next Steps
Offline-First Learn offline-first patterns with Onyx
API Reference Understand API integration with Onyx
Testing Test components using Onyx
Best Practices Follow Onyx best practices