77 * to get the stable channel reference and creates the transport once on
88 * first render (via useRef).
99 *
10- * The transport is closed synchronously when the provider unmounts (via
11- * useLayoutEffect) so that any in-progress operations are aborted before
12- * the channel is detached by ChannelProvider.
10+ * The transport is closed when the provider truly unmounts. The close is
11+ * scheduled as a microtask so that React Strict Mode's synchronous
12+ * remount cycle (mount → fake-unmount → remount) can cancel it before it
13+ * fires, avoiding unnecessary transport teardown in development.
1314 *
1415 * Multiple TransportProviders can be nested using distinct channelNames.
1516 * Each provider merges its transport into the parent record, so descendants
@@ -45,6 +46,8 @@ const TransportProviderInner = <TEvent, TMessage>({
4546 const transportRef = useRef < ClientTransport < TEvent , TMessage > | undefined > ( undefined ) ;
4647 const transportChannelRef = useRef < string > ( channelName ) ;
4748 const transportsToDisposeRef = useRef < ClientTransport < unknown , unknown > [ ] > ( [ ] ) ;
49+ const pendingCloseRef = useRef ( false ) ;
50+
4851
4952 if ( ! transportRef . current || transportChannelRef . current !== channelName ) {
5053 transportChannelRef . current = channelName ;
@@ -70,15 +73,24 @@ const TransportProviderInner = <TEvent, TMessage>({
7073 } ,
7174 [ channelName ] ,
7275 ) ;
73-
74- // Synchronously clear the ref on unmount so stale consumers can't call the closed transport.
75- useLayoutEffect (
76- ( ) => ( ) => {
77- void transportRef . current ?. close ( ) ;
78- transportRef . current = undefined ;
79- } ,
80- [ ] ,
81- ) ;
76+
77+ // Close the transport when the component truly unmounts. The close is
78+ // scheduled as a microtask: in React Strict Mode (dev) the component
79+ // remounts synchronously before any microtask can drain, so the remount's
80+ // effect setup resets pendingCloseRef.current = false and cancels the
81+ // close. On a real unmount no remount follows, the microtask fires, and
82+ // the transport is closed.
83+ useEffect ( ( ) => {
84+ pendingCloseRef . current = false ;
85+ return ( ) => {
86+ pendingCloseRef . current = true ;
87+ void Promise . resolve ( ) . then ( ( ) => {
88+ if ( pendingCloseRef . current ) {
89+ void transportRef . current ?. close ( ) ;
90+ }
91+ } ) ;
92+ }
93+ } , [ ] ) ;
8294
8395 return (
8496 < TransportContext . Provider value = { contextValue } >
0 commit comments