77 * to get the stable channel reference and creates the transport once on
88 * first render (via useRef).
99 *
10+ * If createClientTransport throws, the error is stored in the TransportSlot
11+ * (alongside an undefined transport) so that useClientTransport can surface it
12+ * as transportError without crashing the component tree.
13+ *
1014 * The transport is closed when the provider truly unmounts. The close is
1115 * scheduled as a microtask so that React Strict Mode's synchronous
1216 * remount cycle (mount → fake-unmount → remount) can cancel it before it
1317 * fires, avoiding unnecessary transport teardown in development.
1418 *
1519 * Multiple TransportProviders can be nested using distinct channelNames.
16- * Each provider merges its transport into the parent record, so descendants
20+ * Each provider merges its slot into the parent record so descendants
1721 * can access all registered transports via useClientTransport(channelName).
1822 */
1923
24+ import * as Ably from 'ably' ;
2025import { ChannelProvider , useChannel } from 'ably/react' ;
2126import { type PropsWithChildren , type ReactNode , useContext , useEffect , useMemo , useRef } from 'react' ;
2227
2328import { createClientTransport } from '../../core/transport/client-transport.js' ;
2429import type { ClientTransport , ClientTransportOptions } from '../../core/transport/types.js' ;
30+ import type { TransportSlot } from '../contexts/transport-context.js' ;
2531import { NearestTransportContext , TransportContext } from '../contexts/transport-context.js' ;
2632
2733/**
@@ -47,25 +53,35 @@ const TransportProviderInner = <TEvent, TMessage>({
4753 const transportChannelRef = useRef < string > ( channelName ) ;
4854 const transportsToDisposeRef = useRef < ClientTransport < unknown , unknown > [ ] > ( [ ] ) ;
4955 const pendingCloseRef = useRef ( false ) ;
56+ const constructionErrorRef = useRef < Ably . ErrorInfo | undefined > ( undefined ) ;
5057
5158 if ( ! transportRef . current || transportChannelRef . current !== channelName ) {
5259 transportChannelRef . current = channelName ;
5360 if ( transportRef . current ) transportsToDisposeRef . current . push ( transportRef . current ) ;
54- transportRef . current = createClientTransport ( { ...transportOptions , channel } ) ;
61+ try {
62+ transportRef . current = createClientTransport ( { ...transportOptions , channel } ) ;
63+ constructionErrorRef . current = undefined ;
64+ } catch ( error ) {
65+ transportRef . current = undefined ;
66+ constructionErrorRef . current = error as Ably . ErrorInfo ;
67+ }
5568 }
5669
5770 const parentMap = useContext ( TransportContext ) ;
5871
59- const contextValue = useMemo (
60- ( ) => ( {
61- ...parentMap ,
62- // CAST: TransportContext stores transports with erased generics.
63- // The generic types are fixed at the TransportProvider<TEvent, TMessage> boundary.
64- [ channelName ] : transportRef . current as ClientTransport < unknown , unknown > ,
65- } ) ,
66- [ channelName , parentMap ] ,
72+ // Capture ref values as locals so useMemo deps track changes correctly.
73+ // CAST: TransportContext stores transports with erased generics.
74+ // The generic types are fixed at the TransportProvider<TEvent, TMessage> boundary.
75+ const currentTransport = transportRef . current as ClientTransport < unknown , unknown > | undefined ;
76+ const currentError = constructionErrorRef . current ;
77+
78+ const slot = useMemo < TransportSlot > (
79+ ( ) => ( { transport : currentTransport , error : currentError } ) ,
80+ [ currentTransport , currentError ] ,
6781 ) ;
6882
83+ const contextValue = useMemo ( ( ) => ( { ...parentMap , [ channelName ] : slot } ) , [ channelName , parentMap , slot ] ) ;
84+
6985 useEffect (
7086 ( ) => ( ) => {
7187 for ( const transport of transportsToDisposeRef . current ) void transport . close ( ) ;
@@ -93,9 +109,7 @@ const TransportProviderInner = <TEvent, TMessage>({
93109
94110 return (
95111 < TransportContext . Provider value = { contextValue } >
96- < NearestTransportContext . Provider value = { transportRef . current as ClientTransport < unknown , unknown > } >
97- { children }
98- </ NearestTransportContext . Provider >
112+ < NearestTransportContext . Provider value = { slot } > { children } </ NearestTransportContext . Provider >
99113 </ TransportContext . Provider >
100114 ) ;
101115} ;
@@ -108,13 +122,17 @@ const TransportProviderInner = <TEvent, TMessage>({
108122 * in `TransportContext` under `channelName`. Descendants call
109123 * {@link useClientTransport} with the same `channelName` to access the transport.
110124 *
125+ * If `createClientTransport` throws during construction, the error is surfaced
126+ * through `useClientTransport` as `transportError` — the component tree does not
127+ * crash and children are still rendered.
128+ *
111129 * ```tsx
112130 * <TransportProvider channelName="ai:demo" codec={UIMessageCodec}>
113131 * <Chat />
114132 * </TransportProvider>
115133 *
116134 * // Inside Chat:
117- * const transport = useClientTransport({ channelName: 'ai:demo' });
135+ * const { transport, transportError } = useClientTransport({ channelName: 'ai:demo' });
118136 * ```
119137 *
120138 * For multiple transports, nest providers with distinct channelNames:
@@ -127,8 +145,8 @@ const TransportProviderInner = <TEvent, TMessage>({
127145 * </TransportProvider>
128146 *
129147 * // Inside App:
130- * const main = useClientTransport({ channelName: 'ai:main' });
131- * const aux = useClientTransport({ channelName: 'ai:aux' });
148+ * const { transport: main } = useClientTransport({ channelName: 'ai:main' });
149+ * const { transport: aux } = useClientTransport({ channelName: 'ai:aux' });
132150 * ```
133151 * @param props - Provider configuration including `channelName`, `codec`, and all other {@link ClientTransportOptions}.
134152 * @returns A React element wrapping children with ChannelProvider and TransportContext.
0 commit comments