Skip to content

Commit bdffe1f

Browse files
committed
merge
1 parent 6510c46 commit bdffe1f

1,272 files changed

Lines changed: 124145 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"violations": {
5+
"type": "array",
6+
"items": {
7+
"type": "object",
8+
"properties": {
9+
"ruleId": {
10+
"type": "string"
11+
},
12+
"path": {
13+
"type": "string"
14+
},
15+
"line": {
16+
"type": "integer"
17+
},
18+
"body": {
19+
"type": "string"
20+
}
21+
},
22+
"required": ["ruleId", "path", "line", "body"]
23+
}
24+
}
25+
},
26+
"required": ["violations"]
27+
}

.claude/scripts/check-compiler.sh

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/bin/bash
2+
3+
# Secure proxy script to run React Compiler compliance check on a single file.
4+
# Validates the filepath before passing it to the underlying npm script.
5+
set -eu
6+
7+
if [[ $# -lt 1 ]]; then
8+
echo "Usage: $0 <filepath>" >&2
9+
exit 1
10+
fi
11+
12+
readonly FILEPATH="$1"
13+
14+
# Strict filepath validation - reject shell metacharacters
15+
if ! [[ "$FILEPATH" =~ ^[a-zA-Z0-9_./@-]+$ ]]; then
16+
echo "Error: Invalid filepath (contains disallowed characters)" >&2
17+
exit 1
18+
fi
19+
20+
npm run react-compiler-compliance-check -- check "$FILEPATH"
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
---
2+
ruleId: CLEAN-REACT-PATTERNS-0
3+
title: React Compiler compliance
4+
---
5+
6+
## [CLEAN-REACT-PATTERNS-0] React Compiler compliance
7+
8+
### Reasoning
9+
10+
React Compiler is enabled in this codebase (`babel-plugin-react-compiler` runs first in both webpack and metro configs). It automatically memoizes components and hooks at the AST level — analyzing data flow, tracking dependencies, and inserting fine-grained caching that is more precise than any hand-written `useMemo`, `useCallback`, or `React.memo`.
11+
12+
Manual memoization is therefore:
13+
14+
1. **Redundant** — the compiler already handles it, so the manual wrapper adds zero value
15+
2. **Harmful** — it interferes with the compiler's optimization model, potentially preventing it from applying its own caching strategy or causing double-wrapping
16+
3. **Noisy** — it clutters the codebase with dependency arrays that must be maintained, reviewed, and debugged
17+
18+
The codebase enforces this via:
19+
- **Babel plugin**: `babel-plugin-react-compiler` in `babel.config.js`
20+
- **ESLint processor**: `eslint-plugin-react-compiler-compat` suppresses redundant lint rules when files compile successfully
21+
- **CI compliance check**: `scripts/react-compiler-compliance-check.ts` blocks PRs with manual memoization in new files
22+
23+
Reference: [React Compiler documentation](https://react.dev/learn/react-compiler)
24+
25+
### Incorrect
26+
27+
#### Incorrect (useCallback)
28+
29+
```tsx
30+
function ReportScreen({reportID}: {reportID: string}) {
31+
const handlePress = useCallback(() => {
32+
Navigation.navigate(ROUTES.REPORT_DETAILS.getRoute(reportID));
33+
}, [reportID]);
34+
35+
return <Button onPress={handlePress} />;
36+
}
37+
```
38+
39+
#### Incorrect (useMemo)
40+
41+
```tsx
42+
function PolicyList({policies}: {policies: Policy[]}) {
43+
const sortedPolicies = useMemo(
44+
() => policies.sort((a, b) => a.name.localeCompare(b.name)),
45+
[policies],
46+
);
47+
48+
return <FlatList data={sortedPolicies} renderItem={renderItem} />;
49+
}
50+
```
51+
52+
#### Incorrect (React.memo)
53+
54+
```tsx
55+
const Avatar = React.memo(function Avatar({source, size}: AvatarProps) {
56+
return <Image source={source} style={getAvatarStyle(size)} />;
57+
});
58+
```
59+
60+
### Correct
61+
62+
#### Correct (plain function — compiler memoizes automatically)
63+
64+
```tsx
65+
function ReportScreen({reportID}: {reportID: string}) {
66+
const handlePress = () => {
67+
Navigation.navigate(ROUTES.REPORT_DETAILS.getRoute(reportID));
68+
};
69+
70+
return <Button onPress={handlePress} />;
71+
}
72+
```
73+
74+
#### Correct (plain expression — compiler memoizes automatically)
75+
76+
```tsx
77+
function PolicyList({policies}: {policies: Policy[]}) {
78+
const sortedPolicies = policies.sort((a, b) => a.name.localeCompare(b.name));
79+
80+
return <FlatList data={sortedPolicies} renderItem={renderItem} />;
81+
}
82+
```
83+
84+
#### Correct (plain component — compiler memoizes automatically)
85+
86+
```tsx
87+
function Avatar({source, size}: AvatarProps) {
88+
return <Image source={source} style={getAvatarStyle(size)} />;
89+
}
90+
```
91+
92+
---
93+
94+
### Review Metadata
95+
96+
#### Verification
97+
98+
Before flagging, verify that the file actually compiles with React Compiler:
99+
100+
```bash
101+
check-compiler.sh <filepath>
102+
```
103+
104+
If the output contains **"Failed to compile"** for the file under review, the rule **does not apply** — the author may have no alternative to manual memoization until the compilation issue is resolved.
105+
106+
#### Condition
107+
108+
The verification step above is a prerequisite. Only flag when the file compiles successfully AND any of these are true in new or modified code:
109+
110+
1. **`useCallback`** — A function is wrapped in `useCallback`. The compiler automatically memoizes closures based on their captured variables.
111+
2. **`useMemo`** — A value is wrapped in `useMemo`. The compiler automatically caches derived values.
112+
3. **`React.memo`** — A component is wrapped in `React.memo` (or `memo` imported from React). The compiler automatically skips re-rendering components whose props haven't changed.
113+
114+
**Response:** Challenge the author: "React Compiler is enabled — remove the manual memoization and restructure the code so the compiler can handle it."
115+
116+
The goal is to fix the root cause (make code compiler-friendly) rather than slap on manual memoization as a workaround.
117+
118+
**Search Patterns:**
119+
- `useCallback\s*\(` — manual callback memoization
120+
- `useMemo\s*\(` — manual value memoization
121+
- `React\.memo\s*\(` or `memo\s*\(` — manual component memoization
122+
- Import statements: `useCallback`, `useMemo` from `react`
123+
124+
**DO NOT flag if:**
125+
- The file does not compile with React Compiler (verified by the compliance check in the Verification section above)
126+
- The code is inside `node_modules/`, `patches/`, or test fixtures
127+
- The manual memoization exists in unchanged lines (pre-existing code not touched by the diff)

.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

0 commit comments

Comments
 (0)