> Fetch clean Markdown by appending `.md` to any page URL under https://signalwire.com/docs or requesting it with the HTTP header `Accept: text/markdown`. The root index at https://signalwire.com/docs/llms.txt lists the available documentation indexes. # Framework Integration 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: 1. **Lifecycle** — when to construct the `SignalWire` client, and when to disconnect it. 2. **State** — how to fold an observable into the framework's reactivity system so your UI re-renders when call state changes. The patterns below cover React. If you're using the [web components](/docs/browser-sdk/v4/guides/web-components) instead of the JS SDK directly, you only need to worry about lifecycle — the components manage their own state through context. ## React ### One client per app 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. ```tsx // src/signalwire-context.tsx import { createContext, useContext, useEffect, useState } from "react"; import { SignalWire, StaticCredentialProvider } from "@signalwire/js"; const Ctx = createContext(null); export function SignalWireProvider({ token, children, }: { token: string; children: React.ReactNode; }) { const [client, setClient] = useState(null); useEffect(() => { const c = new SignalWire(new StaticCredentialProvider({ token })); setClient(c); return () => { c.disconnect(); }; }, [token]); return {children}; } export const useSignalWire = () => useContext(Ctx); ``` Keeping one client for the lifetime of the app doesn't mean the user is always reachable. Use [`register()`](/docs/browser-sdk/v4/reference/signalwire/register) and [`unregister()`](/docs/browser-sdk/v4/reference/signalwire/unregister) to opt in and out of inbound calls without tearing down the WebSocket, and observe [`isRegistered$`](/docs/browser-sdk/v4/reference/signalwire/is-registered\$) to drive an "available / away" toggle in your UI. ### Subscribing to observables 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. ```ts // src/hooks/use-observable.ts import { useEffect, useState } from "react"; import type { Observable } from "rxjs"; export function useObservable(obs: Observable | undefined, initial: T) { const [value, setValue] = useState(initial); useEffect(() => { if (!obs) return; const sub = obs.subscribe(setValue); return () => sub.unsubscribe(); }, [obs]); return value; } ``` ```tsx function CallStatus({ call }: { call: WebRTCCall }) { const status = useObservable(call.status$, "idle"); return {status}; } ``` 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. #### Correct ```tsx const doubled$ = useMemo( () => source$.pipe(map((x) => x * 2)), [source$] ); const value = useObservable(doubled$, 0); ``` Stable identity — one subscription for the life of the component. #### Incorrect ```tsx const value = useObservable( source$.pipe(map((x) => x * 2)), 0 ); ``` A fresh observable every render — React unsubscribes and resubscribes on each one. ### Strict Mode and double mounting 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`](https://react.dev/reference/react/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`. ### Typed refs for web components If you're using `@signalwire/web-components`, the package ships a JSX type declaration so `useRef` is fully typed: ```json // tsconfig.json { "compilerOptions": { "types": ["@signalwire/web-components/react"] } } ``` ```tsx import type { SwCallWidget } from "@signalwire/web-components"; function Dialer() { const widget = useRef(null); return ( <> ); } ``` ## Patterns that apply everywhere ### Subscribe immediately 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. ### Always unsubscribe 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](/docs/browser-sdk/v4/guides/rxjs-primer) for the patterns the SDK relies on. ### One client per session, not one per component 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. ### Disconnect on unmount 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. ### Snapshot getters for one-off reads Most observables in the SDK come with a matching snapshot getter that returns the current value synchronously — for example [`audioMuted`](/docs/browser-sdk/v4/reference/self-participant/audio-muted\$) 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: ```ts if (selfParticipant.audioMuted) { await selfParticipant.unmute(); } ``` Reach for the `$` observable only when you actually want your UI to react to changes over time.