Commit 74f27bc
Skip duplicate handoff exchange under React Strict Mode (#10)
* Skip duplicate handoff exchange under React Strict Mode
The bootstrap effect in useManagedAuthSession calls exchangeHandoffCode
on mount with no "already started" guard. Under React 18+ Strict Mode,
effects run mount → cleanup → mount in dev: the first mount consumes
the handoff code server-side, and the second mount re-fires the
exchange with an already-consumed code. The component lands in the
error state and fires onError("Failed to start session"), even though
auth would have worked fine.
The existing ``cancelled`` flag only stops React state updates; it
can't unsend the HTTP request that consumed the code on the first
mount. Add a useRef guard keyed on (sessionId, handoffCode) — same
pattern as the existing callbackFiredRef — so the second mount
short-circuits while a genuine prop change still re-runs the effect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Adopt in-flight exchange via ref identity instead of cancelled flag
Cursor flagged the original guard: under Strict Mode's
mount → cleanup → mount cycle, the cleanup flipped the closure-local
``cancelled`` to true while the second mount short-circuited at the
new ref guard. The first mount's in-flight exchange would then
resolve, read ``cancelled === true``, and skip its own ``setJwt``
call — leaving the component silently stuck with jwt === null after
the server had already consumed the handoff code. That's strictly
worse than the original bug (visible error → invisible dead state).
Replace the closure-local flag with a ref-identity check. Each
distinct (sessionId, handoffCode) gets a fresh ``{ key }`` object on
``exchangeRef``; the in-flight async reads the ref and bails iff the
ref has been *replaced* (genuine prop change). A Strict Mode re-mount
sees the same key and returns early without touching the ref — so
when the first mount's async resolves, ``exchangeRef.current === ref``
still holds and the JWT is committed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Track active flag on exchangeRef + always return cleanup
Cursor flagged two correct bugs in 90c173b:
1. The short-circuit ``return;`` on the Strict Mode second mount
returned undefined, replacing the first mount's cleanup — so when
the component actually unmounted later, ``stopPolling`` never ran
and any started interval/timeout leaked.
2. Without the closure-local ``cancelled`` flag, a real unmount no
longer signaled the in-flight async to stop. The ref-identity
check only caught a *prop change*, not unmount, so the async kept
firing ``setState``/``startPolling`` against a dead component.
Both fix together by adding ``active: boolean`` to the ref object.
Cleanup flips it false; the matching-key remount flips it back true.
The async checks both ``exchangeRef.current !== ref`` (stale identity)
and ``!ref.active`` (real unmount) before each ``setState``. And the
short-circuit path now returns the same cleanup function so React
preserves it across the synthetic unmount/remount.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Tighten Strict Mode comments per review
- Drop the parenthetical pointing at the review tool — process refs
don't belong in published source.
- Collapse the exchangeRef declaration block to one short sentence
naming the fields; defer the invariants to the effect comment so
the strict-mode rationale lives in exactly one place.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 52ebb75 commit 74f27bc
2 files changed
Lines changed: 49 additions & 8 deletions
File tree
- .changeset
- packages/managed-auth-react/src/session
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
Lines changed: 44 additions & 8 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
89 | 94 | | |
90 | 95 | | |
91 | 96 | | |
| |||
167 | 172 | | |
168 | 173 | | |
169 | 174 | | |
170 | | - | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
171 | 210 | | |
172 | 211 | | |
173 | 212 | | |
174 | 213 | | |
175 | 214 | | |
176 | 215 | | |
177 | 216 | | |
178 | | - | |
| 217 | + | |
179 | 218 | | |
180 | 219 | | |
181 | | - | |
| 220 | + | |
182 | 221 | | |
183 | 222 | | |
184 | 223 | | |
| |||
213 | 252 | | |
214 | 253 | | |
215 | 254 | | |
216 | | - | |
| 255 | + | |
217 | 256 | | |
218 | 257 | | |
219 | 258 | | |
| |||
224 | 263 | | |
225 | 264 | | |
226 | 265 | | |
227 | | - | |
228 | | - | |
229 | | - | |
230 | | - | |
| 266 | + | |
231 | 267 | | |
232 | 268 | | |
233 | 269 | | |
| |||
0 commit comments