| name | onyx |
| description | Onyx state management patterns — useOnyx hook, action files, optimistic updates, collections, and offline-first architecture. Use when working with Onyx connections, writing action files, debugging state, or implementing API calls with optimistic data. |
Core Concepts
Onyx is a persistent storage solution wrapped in a Pub/Sub library that enables reactive, offline-first data management — key-value storage with automatic AsyncStorage persistence, reactive subscriptions, and collection management.
For the full API reference (initialization, storage providers, cache eviction, benchmarks, Redux DevTools), see https://github.com/Expensify/react-native-onyx/blob/main/README.md.
Common Patterns
Action File Pattern
IMPORTANT: Onyx state must only be modified from action files (src/libs/actions/). Never call Onyx.merge, Onyx.set, Onyx.clear, or API.write directly from a component.
import Onyx from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';
function setIsOffline(isNetworkOffline: boolean, reason = '') {
if (reason) {
Log.info(`[Network] Client is ${isNetworkOffline ? 'offline' : 'online'} because: ${reason}`);
}
Onyx.merge(ONYXKEYS.NETWORK, {isOffline: isNetworkOffline});
}
export {setIsOffline};
Optimistic Updates Pattern
Optimistic updates allow users to see changes immediately while the API request is queued. This is fundamental to Expensify's offline-first architecture.
For which pattern to use (A / B / C / D) and UX behavior for each, see https://github.com/Expensify/App/blob/main/contributingGuides/philosophies/OFFLINE.md.
Understanding the Three Data Sets
CRITICAL: Backend response data is automatically applied via Pusher updates or HTTPS responses. You do NOT manually set backend data in successData/failureData — only UI state cleanup goes there.
-
optimisticData (Applied immediately, before the API call)
- Mirrors what the backend would return on success
- Gives the user instant feedback without waiting for the server
- Often includes
pendingAction to flag the change as in-flight (e.g. greying out a comment while offline)
pendingAction is cleared once successData or failureData is applied
-
successData (Applied when API succeeds)
- Used for UI state cleanup: clearing
pendingAction, setting isLoading: false
- For
add actions: often not needed (optimisticData already set the right state)
- For
update/delete actions: include to clear pending state
-
failureData (Applied when API fails)
- Reverts optimisticData changes
- Clears
pendingAction.
- Adds
errors field for the user to see
- Always include this to handle unexpected failures
For code examples of each pattern (A/B, loading state, finallyData), see offline-patterns.md.
Performance Optimization
1. Subscribe to Specific Collection Members
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const myReport = allReports[`report_${reportID}`];
const [myReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
2. Use Selectors to Narrow Re-renders
const accountIDSelector = (account: Account) => account?.accountID;
const [accountID] = useOnyx(ONYXKEYS.ACCOUNT, {selector: accountIDSelector});
useOnyx caches by selector reference — a new function reference on every render bypasses the cache and causes unnecessary re-renders. Prefer pure selectors defined in src/selectors/ over inline functions. If a selector must be defined inside a component, ensure referential stability: React Compiler handles this automatically, but in components that are not compiled, wrap the selector in useMemo.
const [accountID] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.accountID});
const selectAccountID = (account: Account) => account?.accountID;
const selector = useMemo(() => (account: Account) => account?.accountID, []);
const [accountID] = useOnyx(ONYXKEYS.ACCOUNT, {selector});
For skipCacheCheck (large objects) and batch collection update patterns, see https://github.com/Expensify/react-native-onyx/blob/main/README.md.
Common Pitfalls
Mixing set and merge on the Same Key
Onyx.set() calls are not batched with Onyx.merge() calls, which can produce race conditions:
Onyx.set(ONYXKEYS.ACCOUNT, null);
Onyx.merge(ONYXKEYS.ACCOUNT, {validated: true});
Onyx.set(ONYXKEYS.ACCOUNT, {validated: true});
Common Tasks Quick Reference
Onyx.merge(ONYXKEYS.NETWORK, {isOffline: true});
Onyx.set(ONYXKEYS.ACCOUNT, null);
const [data] = useOnyx(ONYXKEYS.SOME_KEY);
const [field] = useOnyx(ONYXKEYS.SOME_KEY, {selector: (data) => data?.specificField});
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {unread: false});
Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, updates);
API.write('SomeCommand', params, {optimisticData, successData, failureData});
Related Files