The Browser SDK is framework-agnostic — every public surface is either
a plain class (SignalWire, StaticCredentialProvider), a method
returning a Promise, or an RxJS observable. Integration with React,
Vue, Svelte, or Angular comes down to two questions:
SignalWire client, and when
to disconnect it.The patterns below cover React. If you’re using the web components instead of the JS SDK directly, you only need to worry about lifecycle — the components manage their own state through context.
Construct the client once at app start, share it through context, and
disconnect it on unmount. Do not put new SignalWire(...) inside
a render — it would re-run on every state change.
Keeping one client for the lifetime of the app doesn’t mean the user is always reachable. Use register() and unregister() to opt in and out of inbound calls without tearing down the WebSocket, and observe isRegistered$ to drive an “available / away” toggle in your UI.
Wrap an observable in a hook that subscribes on mount and unsubscribes on unmount. The SDK uses BehaviorSubjects, so the current value emits synchronously on subscribe.
If you’re deriving an observable on the fly with operators like pipe,
wrap it in useMemo so its identity is stable across renders.
Otherwise the useEffect dependency in useObservable sees a new
observable every render and resubscribes on each one.
Stable identity — one subscription for the life of the component.
React 18 Strict Mode mounts components twice in development to surface
unsafe lifecycle effects. The pattern above is safe because the
cleanup function disconnects the client — but if you await client.connect() outside useEffect, you’ll get a duplicate
connection. Always put side effects inside useEffect.
If you’d rather not roll your own subscription hook, React 18+ ships
useSyncExternalStore,
which is designed for exactly this — subscribing to an external store
in a way that’s safe under concurrent rendering and Strict Mode. You
can adapt the useObservable hook above to call it instead of
useState + useEffect.
If you’re using @signalwire/web-components, the package ships a JSX
type declaration so useRef<SwCallWidget> is fully typed:
The SDK uses BehaviorSubjects throughout. They emit their current
value synchronously on subscribe — but only if you actually subscribe.
A common bug is awaiting client.ready$.pipe(filter(Boolean), take(1))
after the client has already become ready, then waiting forever.
Subscribe early; you’ll get the cached value.
Memory leaks in long-lived sessions are almost always missing
unsubscribes. Use the framework’s lifecycle hook (useEffect cleanup,
onUnmounted, onDestroy, the async pipe) and resist the urge to
“just keep it simple.” See the RxJS Primer
for the patterns the SDK relies on.
Constructing a SignalWire opens a WebSocket. Sharing the client
through context (React), provide/inject (Vue), getContext (Svelte),
or a singleton service (Angular) avoids “why am I seeing two
connections in the network panel” debugging.
The SDK doesn’t tear down its WebSocket when the host page is hot-
reloaded — your cleanup hook has to call client.disconnect(). This
matters most in dev mode (Vite, Next.js fast refresh) where unmount /
mount cycles are frequent.
Most observables in the SDK come with a matching snapshot getter that
returns the current value synchronously — for example
audioMuted
alongside audioMuted$. When you just need the value at a single
point in time (inside an event handler, before issuing a command, in
a one-shot log line), read the snapshot directly instead of
subscribing:
Reach for the $ observable only when you actually want your UI to
react to changes over time.