For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
Log inSign up
Support
GuidesReferenceClick-to-Call
GuidesReferenceClick-to-Call
  • Getting Started
    • Overview
    • Authentication
    • RxJS Primer
    • Migrate from v3
  • Web Components
    • Overview
    • Click-to-Call
    • Theming
    • Customization
  • Build Voice & Video Apps
    • Overview
    • Outbound Calls
    • Inbound Calls
    • Device Management
    • Screen Sharing
    • Call Controls
    • Layouts
    • Messaging & Chat
  • Manage Resources
    • Overview
    • Users
    • Address Book
    • Client Preferences
    • Capabilities
  • Deploy
    • Overview
    • Framework Integration
    • SSR & Next.js
    • Troubleshooting
LogoLogoSignalWire Docs
Log inSign up
Support
On this page
  • React
  • One client per app
  • Subscribing to observables
  • Strict Mode and double mounting
  • Typed refs for web components
  • Patterns that apply everywhere
  • Subscribe immediately
  • Always unsubscribe
  • One client per session, not one per component
  • Disconnect on unmount
  • Snapshot getters for one-off reads
Deploy

Framework Integration

|View as Markdown|Open in Claude|
Was this page helpful?
Edit this page
Previous

SSR & Next.js

Next
Built with

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

1// src/signalwire-context.tsx
2import { createContext, useContext, useEffect, useState } from "react";
3import { SignalWire, StaticCredentialProvider } from "@signalwire/js";
4
5const Ctx = createContext<SignalWire | null>(null);
6
7export function SignalWireProvider({
8 token,
9 children,
10}: {
11 token: string;
12 children: React.ReactNode;
13}) {
14 const [client, setClient] = useState<SignalWire | null>(null);
15
16 useEffect(() => {
17 const c = new SignalWire(new StaticCredentialProvider({ token }));
18 setClient(c);
19 return () => {
20 c.disconnect();
21 };
22 }, [token]);
23
24 return <Ctx.Provider value={client}>{children}</Ctx.Provider>;
25}
26
27export const useSignalWire = () => useContext(Ctx);

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.

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.

1// src/hooks/use-observable.ts
2import { useEffect, useState } from "react";
3import type { Observable } from "rxjs";
4
5export function useObservable<T>(obs: Observable<T> | undefined, initial: T) {
6 const [value, setValue] = useState<T>(initial);
7 useEffect(() => {
8 if (!obs) return;
9 const sub = obs.subscribe(setValue);
10 return () => sub.unsubscribe();
11 }, [obs]);
12 return value;
13}
1function CallStatus({ call }: { call: WebRTCCall }) {
2 const status = useObservable(call.status$, "idle");
3 return <span>{status}</span>;
4}

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
Incorrect
1const doubled$ = useMemo(
2 () => source$.pipe(map((x) => x * 2)),
3 [source$]
4);
5
6const value = useObservable(doubled$, 0);

Stable identity — one subscription for the life of the component.

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, 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:

1// tsconfig.json
2{
3 "compilerOptions": {
4 "types": ["@signalwire/web-components/react"]
5 }
6}
1import type { SwCallWidget } from "@signalwire/web-components";
2
3function Dialer() {
4 const widget = useRef<SwCallWidget>(null);
5 return (
6 <>
7 <sw-call-widget ref={widget} token="…" destination="/public/sales" />
8 <button onClick={() => widget.current?.dial()}>Dial</button>
9 </>
10 );
11}

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 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 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:

1if (selfParticipant.audioMuted) {
2 await selfParticipant.unmute();
3}

Reach for the $ observable only when you actually want your UI to react to changes over time.