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
1617 * can access all registered transports via useClientTransport(channelName).
1718 */
1819
1920import { ChannelProvider , useChannel } from 'ably/react' ;
20- import { type PropsWithChildren , type ReactNode , useContext , useEffect , useLayoutEffect , useMemo , useRef } from 'react' ;
21+ import { type PropsWithChildren , type ReactNode , useContext , useEffect , useMemo , useRef } from 'react' ;
2122
2223import { createClientTransport } from '../../core/transport/client-transport.js' ;
2324import type { ClientTransport , ClientTransportOptions } from '../../core/transport/types.js' ;
@@ -45,6 +46,7 @@ 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 ) ;
4850
4951 if ( ! transportRef . current || transportChannelRef . current !== channelName ) {
5052 transportChannelRef . current = channelName ;
@@ -71,14 +73,23 @@ const TransportProviderInner = <TEvent, TMessage>({
7173 [ channelName ] ,
7274 ) ;
7375
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+ // Close the transport when the component truly unmounts. The close is
77+ // scheduled as a microtask: in React Strict Mode (dev) the component
78+ // remounts synchronously before any microtask can drain, so the remount's
79+ // effect setup resets pendingCloseRef.current = false and cancels the
80+ // close. On a real unmount no remount follows, the microtask fires, and
81+ // the transport is closed.
82+ useEffect ( ( ) => {
83+ pendingCloseRef . current = false ;
84+ return ( ) => {
85+ pendingCloseRef . current = true ;
86+ void Promise . resolve ( ) . then ( ( ) => {
87+ if ( pendingCloseRef . current ) {
88+ void transportRef . current ?. close ( ) ;
89+ }
90+ } ) ;
91+ } ;
92+ } , [ ] ) ;
8293
8394 return (
8495 < TransportContext . Provider value = { contextValue } >
0 commit comments