> For a complete index of all SignalWire documentation pages, fetch https://signalwire.com/docs/llms.txt

# 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<SignalWire | null>(null);

export function SignalWireProvider({
  token,
  children,
}: {
  token: string;
  children: React.ReactNode;
}) {
  const [client, setClient] = useState<SignalWire | null>(null);

  useEffect(() => {
    const c = new SignalWire(new StaticCredentialProvider({ token }));
    setClient(c);
    return () => {
      c.disconnect();
    };
  }, [token]);

  return <Ctx.Provider value={client}>{children}</Ctx.Provider>;
}

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<T>(obs: Observable<T> | undefined, initial: T) {
  const [value, setValue] = useState<T>(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 <span>{status}</span>;
}
```

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.

```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.

```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<SwCallWidget>` 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<SwCallWidget>(null);
  return (
    <>
      <sw-call-widget ref={widget} token="…" destination="/public/sales" />
      <button onClick={() => widget.current?.dial()}>Dial</button>
    </>
  );
}
```

## 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.