Skip to content

Commit 11d8cc0

Browse files
merge main and fix conflicts v2
2 parents 4aab126 + 1e72ec1 commit 11d8cc0

File tree

307 files changed

+8880
-1956
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

307 files changed

+8880
-1956
lines changed

.claude/skills/onyx/SKILL.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
---
2+
name: onyx
3+
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.
4+
---
5+
6+
## Core Concepts
7+
8+
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.
9+
10+
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.
11+
12+
## Common Patterns
13+
14+
### Action File Pattern
15+
16+
**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.
17+
18+
```typescript
19+
import Onyx from 'react-native-onyx';
20+
import ONYXKEYS from '@src/ONYXKEYS';
21+
22+
function setIsOffline(isNetworkOffline: boolean, reason = '') {
23+
if (reason) {
24+
Log.info(`[Network] Client is ${isNetworkOffline ? 'offline' : 'online'} because: ${reason}`);
25+
}
26+
Onyx.merge(ONYXKEYS.NETWORK, {isOffline: isNetworkOffline});
27+
}
28+
29+
export {setIsOffline};
30+
```
31+
32+
### Optimistic Updates Pattern
33+
34+
Optimistic updates allow users to see changes immediately while the API request is queued. This is fundamental to Expensify's offline-first architecture.
35+
36+
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.
37+
38+
#### Understanding the Three Data Sets
39+
40+
**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.
41+
42+
1. **optimisticData** (Applied immediately, before the API call)
43+
- Mirrors what the backend would return on success
44+
- Gives the user instant feedback without waiting for the server
45+
- Often includes `pendingAction` to flag the change as in-flight (e.g. greying out a comment while offline)
46+
- `pendingAction` is cleared once `successData` or `failureData` is applied
47+
48+
2. **successData** (Applied when API succeeds)
49+
- Used for UI state cleanup: clearing `pendingAction`, setting `isLoading: false`
50+
- For `add` actions: often not needed (optimisticData already set the right state)
51+
- For `update`/`delete` actions: include to clear pending state
52+
53+
3. **failureData** (Applied when API fails)
54+
- Reverts optimisticData changes
55+
- Clears `pendingAction`.
56+
- Adds `errors` field for the user to see
57+
- Always include this to handle unexpected failures
58+
59+
For code examples of each pattern (A/B, loading state, `finallyData`), see [offline-patterns.md](offline-patterns.md).
60+
61+
## Performance Optimization
62+
63+
### 1. Subscribe to Specific Collection Members
64+
65+
```typescript
66+
// BAD: re-renders on any report change
67+
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
68+
const myReport = allReports[`report_${reportID}`];
69+
70+
// GOOD: re-renders only when this report changes
71+
const [myReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
72+
```
73+
74+
### 2. Use Selectors to Narrow Re-renders
75+
76+
```typescript
77+
const accountIDSelector = (account: Account) => account?.accountID;
78+
const [accountID] = useOnyx(ONYXKEYS.ACCOUNT, {selector: accountIDSelector});
79+
```
80+
81+
`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`.
82+
83+
```typescript
84+
// BAD: new function reference on every render defeats caching
85+
const [accountID] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.accountID});
86+
87+
// GOOD: stable reference defined outside the component
88+
// src/selectors/accountSelectors.ts
89+
const selectAccountID = (account: Account) => account?.accountID;
90+
91+
// GOOD: stable reference via useMemo (for non-React-Compiler components)
92+
const selector = useMemo(() => (account: Account) => account?.accountID, []);
93+
const [accountID] = useOnyx(ONYXKEYS.ACCOUNT, {selector});
94+
```
95+
96+
For `skipCacheCheck` (large objects) and batch collection update patterns, see https://github.com/Expensify/react-native-onyx/blob/main/README.md.
97+
98+
## Common Pitfalls
99+
100+
### Mixing set and merge on the Same Key
101+
102+
`Onyx.set()` calls are not batched with `Onyx.merge()` calls, which can produce race conditions:
103+
104+
```typescript
105+
// BAD: merge may execute before set resolves
106+
Onyx.set(ONYXKEYS.ACCOUNT, null);
107+
Onyx.merge(ONYXKEYS.ACCOUNT, {validated: true});
108+
109+
// GOOD: use one operation
110+
Onyx.set(ONYXKEYS.ACCOUNT, {validated: true});
111+
```
112+
113+
## Common Tasks Quick Reference
114+
115+
```typescript
116+
// Update a single field
117+
Onyx.merge(ONYXKEYS.NETWORK, {isOffline: true});
118+
119+
// Delete data
120+
Onyx.set(ONYXKEYS.ACCOUNT, null);
121+
122+
// Subscribe in component
123+
const [data] = useOnyx(ONYXKEYS.SOME_KEY);
124+
125+
// Subscribe with selector
126+
const [field] = useOnyx(ONYXKEYS.SOME_KEY, {selector: (data) => data?.specificField});
127+
128+
// Update collection member
129+
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {unread: false});
130+
131+
// Batch update collection
132+
Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, updates);
133+
134+
// API call with optimistic update
135+
API.write('SomeCommand', params, {optimisticData, successData, failureData});
136+
```
137+
138+
## Related Files
139+
140+
- https://github.com/Expensify/react-native-onyx/blob/main/README.md - Full Onyx API reference (initialization, merge/set/connect, collections, loading state, cache eviction, Redux DevTools, benchmarks)
141+
- https://github.com/Expensify/App/blob/main/contributingGuides/philosophies/OFFLINE.md - Full offline UX patterns, decision flowchart, and when to use each pattern (A/B/C/D)
142+
- [offline-patterns.md](offline-patterns.md) - Code examples for each optimistic update pattern
143+
- https://github.com/Expensify/App/blob/main/src/ONYXKEYS.ts - All Onyx key definitions
144+
- https://github.com/Expensify/App/tree/main/src/libs/actions - Action files that update Onyx
145+
- https://github.com/Expensify/App/blob/main/src/hooks/useOnyx.ts - useOnyx hook implementation
146+
- https://github.com/Expensify/App/tree/main/src/types/onyx - TypeScript types for Onyx data
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Optimistic Patterns Code Examples
2+
3+
## Pattern A: Optimistic Without Feedback
4+
5+
No `successData`/`failureData` — fire and forget.
6+
7+
```typescript
8+
function pinReport(reportID: string) {
9+
const optimisticData: OnyxUpdate[] = [
10+
{
11+
onyxMethod: Onyx.METHOD.MERGE,
12+
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
13+
value: {isPinned: true},
14+
},
15+
];
16+
17+
API.write('TogglePinnedChat', {reportID}, {optimisticData});
18+
}
19+
```
20+
21+
## Pattern B: Optimistic With Feedback
22+
23+
Show pending state; revert or clean up on completion.
24+
25+
```typescript
26+
function deleteReport(reportID: string) {
27+
const optimisticData: OnyxUpdate[] = [
28+
{
29+
onyxMethod: Onyx.METHOD.MERGE,
30+
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
31+
value: {
32+
statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
33+
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
34+
},
35+
},
36+
];
37+
38+
const successData: OnyxUpdate[] = [
39+
{
40+
onyxMethod: Onyx.METHOD.SET,
41+
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
42+
value: null,
43+
},
44+
];
45+
46+
const failureData: OnyxUpdate[] = [
47+
{
48+
onyxMethod: Onyx.METHOD.MERGE,
49+
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
50+
value: {
51+
statusNum: null,
52+
pendingAction: null,
53+
errors: {[DateUtils.getMicroseconds()]: 'Failed to delete report'},
54+
},
55+
},
56+
];
57+
58+
API.write('DeleteReport', {reportID}, {optimisticData, successData, failureData});
59+
}
60+
```
61+
62+
## Example with Loading State
63+
64+
```typescript
65+
function sendMessage(reportID: string, text: string) {
66+
const optimisticData: OnyxUpdate[] = [
67+
{
68+
onyxMethod: Onyx.METHOD.MERGE,
69+
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
70+
value: {
71+
isLoading: true,
72+
lastMessageText: text,
73+
},
74+
},
75+
];
76+
77+
const successData: OnyxUpdate[] = [
78+
{
79+
onyxMethod: Onyx.METHOD.MERGE,
80+
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
81+
value: {
82+
isLoading: false,
83+
pendingAction: null,
84+
},
85+
},
86+
];
87+
88+
const failureData: OnyxUpdate[] = [
89+
{
90+
onyxMethod: Onyx.METHOD.MERGE,
91+
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
92+
value: {
93+
isLoading: false,
94+
lastMessageText: null,
95+
pendingAction: null,
96+
errors: {[DateUtils.getMicroseconds()]: 'Failed to send message'},
97+
},
98+
},
99+
];
100+
101+
API.write('AddComment', {reportID, text}, {optimisticData, successData, failureData});
102+
}
103+
```
104+
105+
## Using finallyData
106+
107+
When `successData` and `failureData` would be identical, use `finallyData` instead:
108+
109+
```typescript
110+
const finallyData: OnyxUpdate[] = [
111+
{
112+
onyxMethod: Onyx.METHOD.MERGE,
113+
key: ONYXKEYS.SOME_KEY,
114+
value: {
115+
isLoading: false,
116+
pendingAction: null,
117+
},
118+
},
119+
];
120+
121+
API.write('SomeCommand', params, {optimisticData, finallyData});
122+
```

.github/actions/javascript/authorChecklist/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15589,7 +15589,7 @@ const CONST = {
1558915589
},
1559015590
COMMENT: {
1559115591
TYPE_BOT: 'Bot',
15592-
NAME_MELVIN_BOT: 'melvin-bot',
15592+
NAME_MELVIN_BOT: 'melvin-bot[bot]',
1559315593
NAME_MELVIN_USER: 'MelvinBot',
1559415594
NAME_CODEX: 'chatgpt-codex-connector',
1559515595
NAME_GITHUB_ACTIONS: 'github-actions',

.github/actions/javascript/awaitStagingDeploys/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12367,7 +12367,7 @@ const CONST = {
1236712367
},
1236812368
COMMENT: {
1236912369
TYPE_BOT: 'Bot',
12370-
NAME_MELVIN_BOT: 'melvin-bot',
12370+
NAME_MELVIN_BOT: 'melvin-bot[bot]',
1237112371
NAME_MELVIN_USER: 'MelvinBot',
1237212372
NAME_CODEX: 'chatgpt-codex-connector',
1237312373
NAME_GITHUB_ACTIONS: 'github-actions',

.github/actions/javascript/checkAndroidStatus/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -737110,7 +737110,7 @@ const CONST = {
737110737110
},
737111737111
COMMENT: {
737112737112
TYPE_BOT: 'Bot',
737113-
NAME_MELVIN_BOT: 'melvin-bot',
737113+
NAME_MELVIN_BOT: 'melvin-bot[bot]',
737114737114
NAME_MELVIN_USER: 'MelvinBot',
737115737115
NAME_CODEX: 'chatgpt-codex-connector',
737116737116
NAME_GITHUB_ACTIONS: 'github-actions',

.github/actions/javascript/checkDeployBlockers/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11634,7 +11634,7 @@ const CONST = {
1163411634
},
1163511635
COMMENT: {
1163611636
TYPE_BOT: 'Bot',
11637-
NAME_MELVIN_BOT: 'melvin-bot',
11637+
NAME_MELVIN_BOT: 'melvin-bot[bot]',
1163811638
NAME_MELVIN_USER: 'MelvinBot',
1163911639
NAME_CODEX: 'chatgpt-codex-connector',
1164011640
NAME_GITHUB_ACTIONS: 'github-actions',

.github/actions/javascript/checkSVGCompression/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20159,7 +20159,7 @@ const CONST = {
2015920159
},
2016020160
COMMENT: {
2016120161
TYPE_BOT: 'Bot',
20162-
NAME_MELVIN_BOT: 'melvin-bot',
20162+
NAME_MELVIN_BOT: 'melvin-bot[bot]',
2016320163
NAME_MELVIN_USER: 'MelvinBot',
2016420164
NAME_CODEX: 'chatgpt-codex-connector',
2016520165
NAME_GITHUB_ACTIONS: 'github-actions',

.github/actions/javascript/createOrUpdateStagingDeploy/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11855,7 +11855,7 @@ const CONST = {
1185511855
},
1185611856
COMMENT: {
1185711857
TYPE_BOT: 'Bot',
11858-
NAME_MELVIN_BOT: 'melvin-bot',
11858+
NAME_MELVIN_BOT: 'melvin-bot[bot]',
1185911859
NAME_MELVIN_USER: 'MelvinBot',
1186011860
NAME_CODEX: 'chatgpt-codex-connector',
1186111861
NAME_GITHUB_ACTIONS: 'github-actions',

.github/actions/javascript/formatCodeCovComment/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11821,7 +11821,7 @@ const CONST = {
1182111821
},
1182211822
COMMENT: {
1182311823
TYPE_BOT: 'Bot',
11824-
NAME_MELVIN_BOT: 'melvin-bot',
11824+
NAME_MELVIN_BOT: 'melvin-bot[bot]',
1182511825
NAME_MELVIN_USER: 'MelvinBot',
1182611826
NAME_CODEX: 'chatgpt-codex-connector',
1182711827
NAME_GITHUB_ACTIONS: 'github-actions',

.github/actions/javascript/getArtifactInfo/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11595,7 +11595,7 @@ const CONST = {
1159511595
},
1159611596
COMMENT: {
1159711597
TYPE_BOT: 'Bot',
11598-
NAME_MELVIN_BOT: 'melvin-bot',
11598+
NAME_MELVIN_BOT: 'melvin-bot[bot]',
1159911599
NAME_MELVIN_USER: 'MelvinBot',
1160011600
NAME_CODEX: 'chatgpt-codex-connector',
1160111601
NAME_GITHUB_ACTIONS: 'github-actions',

0 commit comments

Comments
 (0)