Skip to main content

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: '[email protected]',
});

// 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

MethodDescriptionExample
SETReplace entire valueReset a policy
MERGEUpdate specific fieldsUpdate policy name
MERGE_COLLECTIONUpdate multiple collection itemsBulk 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)

Performance Optimization

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

4. Metadata Pattern

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: '[email protected]',
  });

  const {getByText} = render(<MyComponent />);
  
  await waitFor(() => {
    expect(getByText('[email protected]')).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: '[email protected]',
});
// 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

Redux DevTools Integration

Enable in .env:
USE_REDUX_DEVTOOLS=true
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

  1. Check that you’re using the correct Onyx key
  2. Verify optimisticData/successData/failureData are set correctly
  3. Check network tab for API responses
  4. Look for errors in failureData being applied

Performance Issues

  1. Use selectors to prevent unnecessary re-renders
  2. Use allowStaleData for non-critical data
  3. Separate data and metadata into different keys
  4. 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

Build docs developers (and LLMs) love