You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: .agents/skills/review-react/SKILL.md
+16Lines changed: 16 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -64,6 +64,22 @@ Reference these guidelines when:
64
64
-`advanced-event-handler-refs` - Store latest event handlers in refs for stable callbacks
65
65
-`advanced-init-once` - Initialize app-level singletons once, not per mount
66
66
67
+
## Review Discipline
68
+
69
+
### Never downgrade CRITICAL violations
70
+
71
+
When a CRITICAL rule violation is detected (e.g., `react-rules-purity`), **fix it — do not rationalize exceptions**. Common rationalizations to reject:
72
+
73
+
- "It's idempotent, so Strict Mode double-render is fine" — Strict Mode is not the only concern; Concurrent Mode render abandonment is the real danger.
74
+
- "It works in practice" — Concurrent features may not be active today but the code must be correct when they are.
75
+
- "Adding a comment explaining the intent is sufficient" — A comment does not prevent the bug. If the rule says "don't do X", the fix is to stop doing X.
76
+
77
+
If you detect a violation, move to "fix" before moving to "judge severity." The cost of a false positive (unnecessary refactor) is far lower than the cost of a false negative (shipping a Concurrent Mode bug).
78
+
79
+
### Re-check after refactors
80
+
81
+
When a fix for one issue changes the code structure (e.g., adding `callback` to useEffect deps), **re-run the full rule check** on the modified code. A fix for one rule can regress another — e.g., fixing `rerender-dependencies` can reintroduce a `react-rules-purity` violation if it moves code back into render phase.
82
+
67
83
## How to Use
68
84
69
85
Read individual rule files for detailed explanations and code examples:
`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
56
+
57
+
### When external code reads your ref: useEffect update is mandatory
58
+
59
+
The "Correct" pattern above (update ref in `useEffect`) is not just an optimization — it becomes a **correctness requirement** when code outside React reads the ref synchronously.
- Framework lifecycle hooks that fire outside React's render cycle
65
+
66
+
If you update the ref during render instead of in useEffect, Concurrent Mode can abandon that render. The external reader then sees a callback from a render that never committed — a value that doesn't correspond to any real UI state.
67
+
68
+
**Rule of thumb:** If anything outside React's tree can call `ref.current`, the ref must only be written in `useEffect`. The one-render staleness between render and commit is the correct trade-off — a stale-but-committed callback is always safer than an uncommitted one.
69
+
70
+
See also: `react-rules-purity` Rule 6 for the full rationale and examples.
Copy file name to clipboardExpand all lines: .agents/skills/review-react/rules/react-rules-purity.md
+45Lines changed: 45 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -118,4 +118,49 @@ function Page({ title }) {
118
118
}
119
119
```
120
120
121
+
### Rule 6: Never write refs during render when external code reads them synchronously
122
+
123
+
Render-phase ref writes (`ref.current = value`) are dangerous when **code outside React** (store subscribers, event bus listeners, framework lifecycle hooks) can read that ref synchronously between renders.
124
+
125
+
In Concurrent Mode, React may **abandon** a render and start a new one with different props. If you wrote to the ref during the abandoned render, external readers see a value from a render that never committed — a value that doesn't correspond to any committed UI state.
126
+
127
+
This is **not** mitigated by the write being "idempotent" — the problem isn't double-writes from Strict Mode, it's writes from renders that are discarded entirely.
128
+
129
+
**Incorrect (render-phase ref write with external reader):**
130
+
131
+
```tsx
132
+
function useEventBusHandler(bus:EventBus, event:string, handler: () =>void) {
133
+
const handlerRef =useRef(handler)
134
+
handlerRef.current=handler// ← render phase write
135
+
136
+
useEffect(() => {
137
+
// External subscriber reads handlerRef on every event
138
+
returnbus.on(event, () =>handlerRef.current())
139
+
}, [bus, event])
140
+
}
141
+
142
+
// Meanwhile, outside React:
143
+
// bus.emit() fires synchronously on state change — may read a ref
144
+
// written by a render that React later abandoned
145
+
```
146
+
147
+
**Correct (ref updated only in useEffect):**
148
+
149
+
```tsx
150
+
function useEventBusHandler(bus:EventBus, event:string, handler: () =>void) {
151
+
const handlerRef =useRef(handler)
152
+
useEffect(() => {
153
+
handlerRef.current=handler// ← commit phase only
154
+
})
155
+
156
+
useEffect(() => {
157
+
returnbus.on(event, () =>handlerRef.current())
158
+
}, [bus, event])
159
+
}
160
+
```
161
+
162
+
The trade-off: between render and commit, the subscriber may read a one-step-old handler. But a stale-but-committed value is always safer than a value from a render that never committed.
163
+
164
+
**Key principle:** Do not make assumptions about React's scheduling. "This render will commit before anything else happens" is never guaranteed in Concurrent Mode. Any optimization based on that assumption will break.
165
+
121
166
Reference: [Components and Hooks must be pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure)
0 commit comments