fix(react): StrictMode cleanup + relax late-handler guard for re-reg#631
Merged
fix(react): StrictMode cleanup + relax late-handler guard for re-reg#631
Conversation
… re-registration useApp's effect cleanup now closes the App (and its transport) instead of only flipping a `mounted` flag. Under React StrictMode dev double-invoke, the abandoned first instance's PostMessageTransport otherwise keeps a window `message` listener and receives every host postMessage alongside the second instance. The late-handler guard from #629 now tracks `_everHadListener` per event and only flags the *first* registration of a one-shot event post-handshake. Re-registration (React useEffect cleanup + re-add on dep change) of an event that had a pre-connect handler is silent, including under `strict: true`.
Contributor
PreviewPreview deployments for this PR have been cleaned up. |
@modelcontextprotocol/ext-apps
@modelcontextprotocol/server-basic-preact
@modelcontextprotocol/server-basic-react
@modelcontextprotocol/server-basic-solid
@modelcontextprotocol/server-basic-svelte
@modelcontextprotocol/server-basic-vanillajs
@modelcontextprotocol/server-basic-vue
@modelcontextprotocol/server-budget-allocator
@modelcontextprotocol/server-cohort-heatmap
@modelcontextprotocol/server-customer-segmentation
@modelcontextprotocol/server-debug
@modelcontextprotocol/server-map
@modelcontextprotocol/server-pdf
@modelcontextprotocol/server-scenario-modeler
@modelcontextprotocol/server-shadertoy
@modelcontextprotocol/server-sheet-music
@modelcontextprotocol/server-system-monitor
@modelcontextprotocol/server-threejs
@modelcontextprotocol/server-transcript
@modelcontextprotocol/server-video-resource
@modelcontextprotocol/server-wiki-explorer
commit: |
If a View double-mounts (e.g. React StrictMode in dev) without closing the previous App, AppBridge now logs a console.warn when it receives a second ui/initialize. Behavior is unchanged — it still responds and the latest appInfo/appCapabilities replace the previous values — this is purely a diagnostic for host implementers.
This was referenced Apr 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three React-friendliness fixes following #629/#623:
useAppcleanup closes theApp. Previously the effect cleanup only setmounted = false, so under React StrictMode's dev double-invoke the abandoned firstApp'sPostMessageTransportkept its windowmessagelistener and received every host postMessage alongside the second instance (JSONRPC ID collisions, double-fired notifications). NowappInstance?.close()runs in cleanup and after a post-unmount connect resolution.Protocol.close()is idempotent and rejects pending requests withConnectionClosed, so the abandoned instance'sui/initializetypically isn't even sent. May help with ontoolinput is not consistently called #476 reports from dev environments.strict) on every one-shot handler registration afterconnect(), which false-positives on the ReactuseEffect([app, dep])re-registration pattern even when a handler was correctly registered viaonAppCreatedfirst. Now a_everHadListenerset tracks per-event whether any handler ever existed; re-registration is silent. Only the genuinely-risky "first handler for this event is post-connect" case warns/throws.AppBridgewarns on a secondui/initialize. Diagnostic only — behavior unchanged (still responds, latestappInfo/appCapabilitieswin). Surfaces View double-mounting to host implementers.ONE_SHOT_EVENTSand_everHadListenerremainprivate— not part of the public API.Test plan
npm test— 332/332 pass (4 new inlate handler registration, 1close() stops further notification delivery, 1AppBridge warns on second ui/initialize)npx tsc --noEmit— 0 errorsstrict: trueapp.close()stopstoolresultdeliveryuseAppin a real browser)