Skip to content

Commit edcc677

Browse files
authored
Merge pull request #81510 from callstack-internal/clean-react-patterns-5
[No QA] ai-reviewer: keep state and subscriptions narrow
2 parents f415997 + 076370f commit edcc677

File tree

1 file changed

+102
-0
lines changed

1 file changed

+102
-0
lines changed

.claude/agents/code-inline-reviewer.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,6 +1189,108 @@ function SaveButton({ getSiblingFormData }: { getSiblingFormData: () => FormData
11891189

11901190
---
11911191

1192+
### [CLEAN-REACT-PATTERNS-5] Keep state and subscriptions narrow
1193+
1194+
- **Search patterns**: Contexts/hooks/stores exposing large bundled objects, providers with many unrelated `useOnyx` calls, state structures mixing unrelated concerns
1195+
1196+
- **Condition**: Flag when a state structure (context, hook, store, or subscription) bundles unrelated concerns together, causing consumers to re-render when data they don't use changes.
1197+
1198+
**Signs of violation:**
1199+
- State provider (context, hook, or store) that bundles unrelated data (e.g., navigation state + list data + cache utilities in one structure)
1200+
- State object where properties serve different purposes and change independently
1201+
- Multiple unrelated subscriptions (`useOnyx`, `useContext`, store selectors) aggregated into a single exposed value
1202+
- Consumers of a state source that only use a subset of the provided values
1203+
1204+
**DO NOT flag if:**
1205+
- State values are cohesive — they change together and serve the same purpose (e.g., `keyboardHeight` + `isKeyboardShown` both relate to keyboard state)
1206+
- The state structure is intentionally designed as an aggregation point and consumers use most/all values
1207+
- Individual `useOnyx` calls without selectors — this is covered by [PERF-11]
1208+
1209+
- **Reasoning**: When unrelated pieces of data are grouped into a single state structure, if an unused part changes, then all consumers re-render unnecessarily. This silently expands render scope, increases coupling, and makes performance regressions hard to detect. Structuring state around cohesive concerns ensures render scope stays predictable and changes remain local.
1210+
1211+
**Distinction from PERF-11**: PERF-11 addresses individual `useOnyx` selector usage. This rule addresses state structure — how multiple values are grouped and exposed to consumers via contexts, hooks, or stores.
1212+
1213+
**Distinction from CLEAN-REACT-PATTERNS-2**: PATTERNS-2 addresses data flow direction — parent shouldn't fetch data just to pass to children. This rule addresses how state is structured and grouped within any state provider.
1214+
1215+
Good (cohesive state — all values serve one purpose):
1216+
1217+
- All state relates to one concern (keyboard)
1218+
- Values change together — no wasted re-renders
1219+
- Derived state computed inline, not stored separately
1220+
1221+
```tsx
1222+
type KeyboardStateContextValue = {
1223+
isKeyboardShown: boolean;
1224+
isKeyboardActive: boolean;
1225+
keyboardHeight: number;
1226+
};
1227+
1228+
function KeyboardStateProvider({children}: ChildrenProps) {
1229+
const [keyboardHeight, setKeyboardHeight] = useState(0);
1230+
const [isKeyboardActive, setIsKeyboardActive] = useState(false);
1231+
1232+
useEffect(() => {
1233+
const showListener = KeyboardEvents.addListener('keyboardDidShow', (e) => {
1234+
setKeyboardHeight(e.height);
1235+
setIsKeyboardActive(true);
1236+
});
1237+
const hideListener = KeyboardEvents.addListener('keyboardDidHide', () => {
1238+
setKeyboardHeight(0);
1239+
setIsKeyboardActive(false);
1240+
});
1241+
return () => {
1242+
showListener.remove();
1243+
hideListener.remove();
1244+
};
1245+
}, []);
1246+
1247+
const contextValue = useMemo(() => ({
1248+
keyboardHeight,
1249+
isKeyboardShown: keyboardHeight !== 0, // Derived, not separate state
1250+
isKeyboardActive,
1251+
}), [keyboardHeight, isKeyboardActive]);
1252+
1253+
return <KeyboardStateContext.Provider value={contextValue}>{children}</KeyboardStateContext.Provider>;
1254+
}
1255+
```
1256+
1257+
Bad (grab-bag state — bundles unrelated concerns):
1258+
1259+
- State provider subscribes to many unrelated Onyx collections
1260+
- Exposed value mixes navigation state, list data, membership data, and cache utilities
1261+
- Any consumer re-renders when ANY subscribed value changes
1262+
1263+
```tsx
1264+
function SidebarOrderedReportsContextProvider({children}) {
1265+
// ❌ Many unrelated Onyx subscriptions bundled together
1266+
const [priorityMode] = useOnyx(ONYXKEYS.NVP_PRIORITY_MODE);
1267+
const [chatReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
1268+
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
1269+
const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
1270+
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
1271+
const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS);
1272+
const [reportsDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT);
1273+
const [betas] = useOnyx(ONYXKEYS.BETAS);
1274+
const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES);
1275+
1276+
// ❌ Context value mixes unrelated concerns
1277+
const contextValue = {
1278+
orderedReports, // List data
1279+
orderedReportIDs, // List data
1280+
currentReportID, // Navigation state
1281+
policyMemberAccountIDs, // Policy membership
1282+
clearLHNCache, // Cache management utility
1283+
};
1284+
1285+
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
1286+
}
1287+
1288+
// A component needing only currentReportID re-renders when orderedReports changes
1289+
// A component needing only policyMemberAccountIDs re-renders when navigation changes
1290+
```
1291+
1292+
---
1293+
11921294
## Instructions
11931295

11941296
1. **First, get the list of changed files and their diffs:**

0 commit comments

Comments
 (0)