Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/react/etc/components-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,30 @@ export interface UserInfo {
name?: string;
}

// @public
export function useRoomConnection(options: UseRoomConnectionOptions): UseRoomConnectionResult;

// @public (undocumented)
export type UseRoomConnectionDetails = {
serverUrl: string;
participantToken: string;
};

// @public (undocumented)
export type UseRoomConnectionOptions = {
getConnectionDetails: () => Promise<UseRoomConnectionDetails>;
onConnectionError?: (err: Error) => void;
room?: Room;
connected?: boolean;
trackPublishOptions?: TrackPublishOptions;
};

// @public (undocumented)
export type UseRoomConnectionResult = {
room: Room;
status: 'idle' | 'connecting' | 'disconnecting';
};

// @public
export function useRoomContext(): Room;

Expand Down
1 change: 1 addition & 0 deletions packages/react/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ export * from './useParticipantAttributes';
export * from './useIsRecording';
export * from './useTextStream';
export * from './useTranscriptions';
export * from './useRoomConnection';
97 changes: 97 additions & 0 deletions packages/react/src/hooks/useRoomConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Mutex, type TrackPublishOptions, Room } from 'livekit-client';
import { useEffect, useMemo, useState } from 'react';
import { useMaybeRoomContext } from '../context';

/** @public */
export type UseRoomConnectionDetails = {
serverUrl: string;
participantToken: string;
};

/** @public */
export type UseRoomConnectionOptions = {
getConnectionDetails: () => Promise<UseRoomConnectionDetails>;
onConnectionError?: (err: Error) => void;

room?: Room;
/** Should the room attempt to connect? If false, the room will disconnect. */
connected?: boolean;
trackPublishOptions?: TrackPublishOptions;
};

/** @public */
export type UseRoomConnectionResult = {
room: Room;

/** What operation is the useRoomConnection hook currently in the midst of performing? */
status: 'idle' | 'connecting' | 'disconnecting';
};

/**
* The `useRoomConnection` hook provides a fully managed way to connect / disconnect from a LiveKit
* room. To control whether the connection is active or not, use `options.connected`.
* @remarks
* Can be called inside a `RoomContext` or by passing a `Room` instance, otherwise creates a Room
* itself.
*
* @example
* ```tsx
* const { room } = useRoomConnection({
* getConnectionDetails: async () => {
* // compute the below value out of band:
* return { serverUrl: "...", participantToken: "..." };
* },
* });
* ```
* @public
*/
export function useRoomConnection(options: UseRoomConnectionOptions): UseRoomConnectionResult {
const roomFromContext = useMaybeRoomContext();
const room = useMemo(() => {
return roomFromContext ?? options.room ?? new Room();
}, [options.room, roomFromContext]);

const connected = options.connected ?? true;

const [status, setStatus] = useState<'idle' | 'connecting' | 'disconnecting'>('idle');

// NOTE: it would on the surface seem that managing a room's connection with a useEffect would be
// straightforward, but `room.disconnect()` is async and useEffect doesn't support async cleanup
// functions, which means `room.connect()` can run in the midst of `room.disconnect()`, causing
// race conditions.
const connectDisconnectLock = useMemo(() => new Mutex(), []);
useEffect(() => {
connectDisconnectLock.lock().then(async (unlock) => {
if (connected) {
setStatus('connecting');
const connectionDetails = await options.getConnectionDetails();
Copy link
Contributor

@lukasIO lukasIO Aug 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we employ this pattern we have to think about what happens if retrieving connection details here fails. E.g. is a user expected to flip connected to false and back to true on an error?

Also wondering if decoupling the two (connection and connectionDetails) makes more sense, as it can speed up the actual connection if connection Details retrieval happens in advance (e.g. like prefetch before you actually click)

try {
await Promise.all([
room.localParticipant.setMicrophoneEnabled(
true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this limits the usage of this hook to scenarios where users connect to rooms with their mic (and only their mic) enabled 🤔

Copy link
Contributor Author

@1egoman 1egoman Aug 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, you are correct, but this doesn't seem bad to me / was intentional.

I suppose maybe it could be nice to be able to configure whether this setMicrophoneEnabled runs here or not, to enable / disable the audio buffering behavior before connection? But besides that, is there a scenario where a user would want to connect to a room with something else like video enabled before room.connect(...) runs?

undefined,
options.trackPublishOptions,
),
room.connect(connectionDetails.serverUrl, connectionDetails.participantToken),
]);
} catch (error) {
options.onConnectionError?.(error as Error);
} finally {
setStatus('idle');
}
} else {
setStatus('disconnecting');
try {
await room.disconnect();
} catch (error) {
options.onConnectionError?.(error as Error);
} finally {
setStatus('idle');
}
}
unlock();
});
}, [connected]);

return { room, status };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

returning the room here might lead to patterns that we don't really want to encourage, i.e. trying to call methods on the room itself again.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I had figured that was kinda the whole point, I'm not too well versed with this package yet. It sounds like the intention maybe is that the room would always be in the RoomContext and that is how other operations should be performed on it, including imperative updates?

}