---
title: Building a Virtual Waiting Room with Daily's React Hooks Library – WebRTC.ventures
description: This post will guide you through adding a virtual waiting room into your video application using Daily’s React Hooks library. While your participants are waiting to enter a private room, you can “show them into” a custom lobby where they will wait until the host accepts them into the meeting.
---

[WebRTC.ventures Blog](https://blog.webrtc.ventures)

# [Building a Virtual Waiting Room with Daily's React Hooks Library – WebRTC.ventures](https://blog.webrtc.ventures/2022/09/building-a-virtual-waiting-room-with-dailys-react-hooks-library)

 Written by [Jen Oppenheimer](https://blog.webrtc.ventures/author/jen-oppenheimer) | Sep 20, 2022 4:00:00 AM

Private rooms are an important feature in every video conferencing application, from virtual interviewing and online education to board meeting breakout sessions, telehealth and more. While your participants are waiting to enter a private room, you can “show them into” a custom lobby waiting room where they will wait until the host accepts them into the meeting. 

[Daily](https://www.daily.co/) is a WebRTC based CPaaS that allows you to add live video and audio to any product. As we mentioned in an [earlier post](https://webrtc.ventures/2022/07/daily-prebuilt-component-vs-video-sdks/), you can start with the Daily Prebuilt user interface or create custom layouts with their client SDKs. 

As a custom development firm specializing in building live video applications, our team at WebRTC.ventures has deep experience using video APIs like Daily to build unique video experiences for our clients’ applications.

This post will guide you through a demo on how to add a custom lobby or waiting room into your video application using [Daily’s React Hooks library](https://docs.daily.co/reference/daily-react-hooks). Daily React Hooks is a helper library for handling common patterns when building [custom](https://docs.daily.co/guides/products/call-object) Daily applications using React. You can see Daily’s demo on how to use the library [here](https://github.com/daily-demos/custom-video-daily-react-hooks)**.**

## Prerequisites

For this example we’ll use [Daily’s demo](https://github.com/daily-demos/custom-video-daily-react-hooks). If you have GIT installed, you can run the command below to clone it locally. You can also click the “Download ZIP” button under code in the repository:

```
git clone https://github.com/daily-demos/custom-video-daily-react-hooks
```

JavaScript

Copy

Once you have the Daily demo downloaded on your local computer, follow their instructions to make it run. 

The rest of this post will build on top of that demo in order to add a waiting room. If you’d like to see the full code we use in this demo, we have posted it in our repository [here](https://github.com/agilityfeat/CustomLobbyDailyReactHooks).

## Setting up a Private Room

By default, the demo application creates a public room where anyone can access the call. In order to have a lobby page, a private room will need to be created. This is the first step.

In order to create private rooms locally, we modify **api.js** file by doing the following: 

- Comment following piece of code:

From

```
const response = await fetch(`${window.location.origin}/api/rooms`, {
   method: 'POST',
   body: JSON.stringify(options),
 });
```

JavaScript

Copy

To

```
// const response = await fetch(`${window.location.origin}/api/rooms`, {
 // method: 'POST',
 // body: JSON.stringify(options),
 // });
```

JavaScript

Copy

Uncomment following piece of code:

From

```
// const response = await fetch(`https://api.daily.co/v1/rooms/`, {
 // method: 'POST',
 // body: JSON.stringify(options),
 // headers: {
 // 'Content-Type': 'application/json',
 // Authorization: 'Bearer ' + process.env.REACT_APP_DAILY_API_KEY,
 // },
 // });
```

JavaScript

Copy

To

```
const response = await fetch(`https://api.daily.co/v1/rooms/`, {
   method: 'POST',
   body: JSON.stringify(options),
   headers: {
     'Content-Type': 'application/json',
     Authorization: 'Bearer ' + process.env.REACT_APP_DAILY_API_KEY,
   },
 });
```

JavaScript

Copy

- Modify *createRoom* function in order to make private rooms instead of public by adding to the options object the privacy property and set this to private:

```
const options = {
   privacy: 'private',
   properties: {
     exp,
   },
};
```

Plain text

Copy

And adding to the properties object enable\_knocking property and set this to true:

```
const options = {
   privacy: 'private',
   properties: {
     exp,
     enable_knocking: true,
   },
 };
```

JavaScript

Copy

***Note:*** *Setting this property to true will enable the participants with no token to request access to the host of the call.*

## Setting a Room Owner

We need to define a user to be the owner of the room who will admit the participants waiting in the lobby. To define an owner, we need to create a token for the owner. This means we are going to need a new call to the API. In our example, the owner will be the user that starts the call. All other users will be participants that must wait until they get accepted.

Add a new method called *createToken*  to the api.js file. This new method will allow to request an owner token:

```
xport async function createToken() {
 const exp = Math.round(Date.now() / 1000) + 60 * 30;
 const options = {
   properties: {
     exp,
     is_owner: true,
   },
 };
 const response = await fetch(`https://api.daily.co/v1/meeting-tokens/`, {
   method: "POST",
   body: JSON.stringify(options),
   headers: {
     "Content-Type": "application/json",
     Authorization: `Bearer ${process.env.REACT_APP_DAILY_API_KEY}`,
   },
 });
 
 return response.json();
}
```

JavaScript

Copy

We are using [https://api.daily.co/v1/meeting-tokens/](https://api.daily.co/v1/meeting-tokens/) url to request an owner token, since we are sending *is\_owner* property as **true**.

The next step is to modify **App.js** file:

- Import recently created *createToken* method:

```
import api, { createToken } from './api';
```

JavaScript

Copy

- Create a new callback function to call *createToken* method, named *createTokenMeeting*:

```
const createTokenMeeting = useCallback(
   () =>
     createToken()
       .then((response) => response.token)
       .catch((error) => {
         console.error("Error creating token", error);
         setRoomUrl(null);
         setAppState(STATE_IDLE);
         setApiError(true);
       }),
   []
 );
```

JavaScript

Copy

Here we are going to return a token. If there is an issue calling the method, then in the error handling we set the room to null, the application state to idle, and set the flag for the api error to **true**.

- Pass *createTokenMeeting* method as props from **HomeScreen** component:

    <HomeScreen

```
<HomeScreen
       createCall={createCall}
       createToken={createTokenMeeting}
       startHairCheck={startHairCheck}
     />
```

JavaScript

Copy

Since the user that starts the call will be the one defined as owner, we need to pass this method to the **HomeScreen** component in order to generate a token when the user starts the call.

Note that “startHairCheck” is how Daily refers to starting their prejoin user interface.

Now, we modify **HomeScreen.js** file in order to create a token when the user start the call:

- Add *createToken* prop:

```
export default function HomeScreen({ createCall, startHairCheck, createToken }) {
```

JavaScript

Copy

- Call *createToken* prop after the room has been created:

```
const startDemo = () => {
   createCall().then((url) => {
     createToken().then((token) => startHairCheck(url, token));
   });
 };
```

JavaScript

Copy

In the above code, we call *createToken* prop. This will return the **token** for the owner which then we use in the *startHairCheck* function. Such a function will join the user to the room, so we will need it here to define the user as the owner when joining.

Now let’s modify *startHairCheck* function in **App.js** file:

```
const startHairCheck = useCallback(async (url, token) => {
   const newCallObject = DailyIframe.createCallObject();
   setRoomUrl(url);
   setCallObject(newCallObject);
   setAppState(STATE_HAIRCHECK);
   await newCallObject.preAuth(token ? { url, token } : { url });
   await newCallObject.startCamera();
 }, []);
```

JavaScript

Copy

In this function, we are going to define a new parameter named *token* to use in the *preAuth* function. If a token is defined, we will send it to define the user as an owner of the room specified in the url.

## Setting call participants to ask to join the call

We have now set the room as private and the user that started the call is the owner. Next, we need to make the participants ask to join the call.

The first thing is to define two new statuses in the application:

1. Waiting State: when the user is waiting to get access.
2. Access Rejected State: when the user gets the access rejected.

We add these two new states to **App.js** file:

```
const STATE_WAITING = 'STATE_WAITING';
const STATE_REJECTED = 'STATE_REJECTED';
```

JavaScript

Copy

Also, we need to save the username that will join the call locally. This way we know who is requesting access to the meeting. To do this, we define a new state variable in **App.js** file:

```
const [localUserName, setLocalUserName] = useState('');
```

JavaScript

Copy

After that, we send the set up function for this state variable to **HairCheck** component as *setUserName* prop:

```
HairCheck
           joinCall={joinCall}
           cancelCall={startLeavingCall}
           setUserName={setLocalUserName}
         />
```

JavaScript

Copy

In **HairCheck.js** file, we add the new prop:

```
export default function HairCheck({ joinCall, cancelCall, setUserName }) {
```

JavaScript

Copy

We use it in *onChange* function:

```
const onChange = (e) => {
   callObject.setUserName(e.target.value);
   setUserName(e.target.value);
 };
```

JavaScript

Copy

By doing this, we now know the name of the user that is trying to join the call. We can use this information to request access using this username. 

We now go back to **App.js** file and change *joinCall* function:

```
const joinCall = useCallback(async () => {
   await callObject.join({ url: roomUrl });
   const { access } = callObject.accessState();
   if (access?.level === 'lobby') {
     setAppState(STATE_WAITING);
     try {
       const { granted } = await callObject.requestAccess({
         name: localUserName,
         access: {
           level: 'full',
         },
       });
       if (granted) {
         setAppState(STATE_JOINED);
         console.log(' Access granted');
       } else {
         setAppState(STATE_REJECTED);
         console.log(' Access denied');
       }
     } catch (error) {
       console.log(error);
     }
   } else {
     setAppState(STATE_JOINED);
   }
 }, [callObject, roomUrl, localUserName]);
```

JavaScript

Copy

This function will do the following:

We are going to make it asynchronous since now we are going to wait for the user to join the call in order to check the access. In the private rooms we have two levels of access to join:

1. lobby: users need to be granted access from an owner to join the call
2. full: user will join directly to the call, for a public room, the access always will be this and for a private room you will be granted with it if you have a token for the room, no matter if it is for an owner or not.

```
await callObject.join({ url: roomUrl });
const { access } = callObject.accessState();
```

JavaScript

Copy

The *join* method of *callObject* is being called, we must check what type of **access** the user has. If the access is **lobby**, we request **full** access to the owner of the call. Otherwise, we set the application state as joined:

```
if (access?.level === 'lobby') {
     <code here>
   }else {
     setAppState(STATE_JOINED);
   }
```

JavaScript

Copy

After we know that the access level is **lobby**, we set the application state to **waiting** state. In doing this, the application will know that the user needs to wait for access to the call:

```
setAppState(STATE_WAITING);
```

JavaScript

Copy

Next, we request access to the call using the requestAccess method of the callObject object and wait for it. We define the name of who is requesting access and the access level requested. In this case, it will be **full** access:

```
const { granted } = await callObject.requestAccess({
         name: localUserName,
         access: {
           level: 'full',
         },
       });
```

JavaScript

Copy

Also, we will see if the user was granted access or not. We need to verify this in order to set the correct application state:

```
if (granted) {
         setAppState(STATE_JOINED);
         console.log(' Access granted');
       } else {
         setAppState(STATE_REJECTED);
         console.log(' Access denied');
       }
```

JavaScript

Copy

## Creating the lobby for the participants

We are going to manage the new states created in the application:

1. Waiting State
2. Access Rejected State

Since we added these two new states, we can remove some events from **App.js** file because they are no longer needed:

First, update *events* constant:

```
const events = ['joined-meeting', 'left-meeting', 'camera-error'];
```

JavaScript

Copy

Then, update *handleNewMeetingState* function:

```
function handleNewMeetingState() {
     switch (callObject.meetingState()) {
       case 'left-meeting':
         callObject.destroy().then(() => {
           setRoomUrl(null);
           setCallObject(null);
           setAppState(STATE_IDLE);
         });
         break;
       default:
         break;
     }
   }
```

JavaScript

Copy

Update *showCall* constant:

```
const showCall = !apiError && [STATE_JOINING, STATE_JOINED].includes(appState);
```

JavaScript

Copy

Create new constants in order to show the lobby of the rejected message, you can add it in the state variables section, below *apiError* session variable:

```
const showWaiting = appState === STATE_WAITING;
 const showRejected = appState === STATE_REJECTED;
```

JavaScript

Copy

Create the lobby by checking *showWaiting* constant in *renderApp* function:

```
f (showWaiting) {
     return (
       <div className="home-screen">
         Please wait while you are being admitted to the call
       </div>
     );
   }
```

JavaScript

Copy

**Note:** Here you can define a custom page or component that the participants will view while waiting.

Create the rejection message by checking *showRejected* constant in *renderApp* function:

```
if (showRejected) {
     return <div className="home-screen">You were rejected to join!</div>;
   }
```

JavaScript

Copy

**Note:** Here you can define a custom page or component that the participants will see if their request to join the call is rejected.

## Notifying the owner when a participant requests access

At this point, we have defined a user as the owner of the room and the participants are able to request access and wait in the lobby until the access is granted or rejected. All that remains is to notify the owner when someone is trying to join the room.

Modify **Call.js** file:

- Import *useEffect* from react library:

```
import React, { useState, useCallback, useMemo, useEffect } from 'react';
```

JavaScript

Copy

- Import *useDaily* and *useWaitingParticipants* from React Hooks library:

```
import {
 useParticipantIds,
 useScreenShare,
 useLocalParticipant,
 useDailyEvent,
 useDaily,
 useWaitingParticipants,
} from '@daily-co/daily-react-hooks';
```

JavaScript

Copy

- Define a new state variable to show when a user is trying to join, you can add this in session variables section below *getMediaError* session variable:

```
const [showAdmit, setShowAdmit] = useState(false);
```

JavaScript

Copy

- Define a constant to get access to the call object (you can add this in session variables section below *remoteParticpantsIds* constant):

```
const callObject = useDaily();
```

JavaScript

Copy

- Define a constant to get access to the *waitingParticipants* array, this array will increase or decrease based on how many participants are trying to join (you can add this below the constant created above):

```
const { waitingParticipants } = useWaitingParticipants();
```

JavaScript

Copy

We can add the following functions after *isAlone* constant declaration:

```
const isAlone = useMemo(
   () => remoteParticipantIds?.length < 1 || screens?.length < 1,
   [remoteParticipantIds, screens],
 );
```

Plain text

Copy

Create a new hook to check if the local user is an owner an the list of waiting participants has at list one participant to set *showAdmit* variable to **true**:

```
useEffect(() => {
   if (localParticipant?.owner && waitingParticipants.length > 0) {
     setShowAdmit(true);
   } else {
     setShowAdmit(false);
   }
 }, [waitingParticipants, callObject, localParticipant?.owner]);
```

JavaScript

Copy

- Create new function to grant access to all waiting participants named *handleAdmit*:

```
const handleAdmit = () => {
   callObject.updateWaitingParticipants({
     '*': {
       grantRequestedAccess: true,
     },
   });
 };
```

JavaScript

Copy

In this function we are using *updateWaitingParticipants* method of *callObject* object to grant access to all waiting participants, specifying **\*** and setting **grantRequestedAccess** property to **true**.

- Create new function to reject access to all waiting participants named *handleReject*:

```
const handleReject = () => {
   callObject.updateWaitingParticipants({
     '*': {
       grantRequestedAccess: false,
     },
   });
 };
```

JavaScript

Copy

This function is very similar to *handleAdmit*. We are using again *updateWaitingParticipants* method of *callObject*, but instead of setting **grantRequestedAccess** property to true, we are going to set this to **false** in order to reject access.

- Create a simple message with two buttons, one for approve and other to reject the access when there is at least one participant waiting, we will know this using *showAdmit* constant in *renderCallScreen* method:

```
{showAdmit ? (
         <div style={{ color: 'white' }}>
           {`Do you want to admit ${waitingParticipants.map((p, index) => { return `${index ? ' ' : ''}${p.name}`; })}`}
           <button type="button" style={{ margin: 4 }} onClick={handleAdmit}>
             Yes
           </button>
           <button type="button" style={{ margin: 4 }} onClick={handleReject}>
             No
           </button>
 
         </div>
       ) : null}
```

JavaScript

Copy

- Remove “Waiting for others card”, so the message to grant access will appear in the right side instead of the card:

<https://blog.webrtc.ventures/hubfs/Imported_Blog_Media/Juan1.png>

## There you have it, our lobby is complete!

When a participant is trying to access the call the lobby should look like this:

<https://blog.webrtc.ventures/hubfs/Imported_Blog_Media/Juan2.png>

And the host will get the following message:

<https://blog.webrtc.ventures/hubfs/Imported_Blog_Media/Juan3.png>

***If you would like a custom Daily live video application built, WebRTC.ventures can help!*** [***Contact us today***](https://webrtc.ventures/contact/)***. ***

[View full post](https://blog.webrtc.ventures/2022/09/building-a-virtual-waiting-room-with-dailys-react-hooks-library)

```json
{
  "@context" : "http://schema.org",
  "@type" : "BlogPosting",
  "author" : {
    "@type" : "Person",
    "name" : "Jen Oppenheimer"
  },
  "dateModified" : "2024-04-03T19:01:45.865Z",
  "datePublished" : "2022-09-20T04:00:00Z",
  "headline" : "Building a Virtual Waiting Room with Daily's React Hooks Library",
  "image" : {
    "@type" : "ImageObject",
    "height" : 500,
    "url" : "https://45353883.fs1.hubspotusercontent-na1.net/hubfs/45353883/Imported_Blog_Media/Default-B-Apr-03-2024-06-59-54-7168-PM.jpg",
    "width" : 800
  },
  "mainEntityOfPage" : "https://blog.webrtc.ventures/2022/09/building-a-virtual-waiting-room-with-dailys-react-hooks-library",
  "publisher" : {
    "@type" : "Organization",
    "logo" : {
      "@type" : "ImageObject",
      "height" : 60,
      "url" : "/hs/hsstatic/content_shared_assets/static-1.4092/img/default-amp-logo.png",
      "width" : 60
    },
    "name" : "WebRTC.ventures Blog"
  }
}
```