DEV Community

Cover image for Build a video chat app in minutes with React and daily-js
kompfner for Daily

Posted on • Updated on • Originally published at

Build a video chat app in minutes with React and daily-js

With video chat apps on the rise for, well, obvious reasons, it’s increasingly important to be able to add video call capabilities to apps and websites quickly. The more customizable those video calls can be, the better for building unique user experiences.

This post walks you through how to build a custom video chat app with React and the Daily API.

Two people smiling in a video call in the React demo app

What we’ll build

In our app, when a user clicks to start a call, the app will create a meeting room, pass the room’s URL to a new Daily call object, and join the call. The call object is something that keeps track of important information about the meeting, like other participants (including their audio and video tracks) and the things they do on the call (e.g. muting their mic or leaving), and provides methods for interacting with the meeting. The app leverages this object to update its state accordingly, and to carry out user actions like muting or screen-sharing. When the user leaves the meeting room, the call object is destroyed.

What you’ll need to build it

  • Daily account: Sign up for an account if you don’t have one already.
  • Cloned daily-demos/call-object-react Github repository: The fastest way to follow along with this tutorial and get a demo app up and running is by cloning this repo.
  • Familiarity with React: In this post we skip over a lot of the code that isn’t related to Daily, so it could be worth brushing up on React and/or hooks [0].

Once you have those things, we’re ready to get started!

Build and run the app

After you’ve cloned the repository:

cd call-object-react
npm i
npm run dev
Enter fullscreen mode Exit fullscreen mode

Now, open your browser and head to localhost:<port>, using the port printed in the terminal after running the above. You should have an app running locally that mirrors the live demo: you can click to start a call, and share the link with another participant or yourself in a new tab.

Anakin Skywalker in Phantom Menace exclaims it’s working

It’s great that it’s working...but how is it working?

How the app works

Daily concepts

Before diving into the code, let’s cover some Daily fundamentals.

The call object

A Daily call object is like a direct line to the Daily API. It gives us the finest-grained control over a video call, letting us access its lowest level foundations, like participant video and audio tracks. Invoking DailyIframe.createCallObject() creates a call object. Once created, we pass a meeting room URL to the call object to join a call.

In addition to all of that, the call object keeps track of our call’s state, both meeting state and participant state.

State #1: meeting state

Meeting state tracks where a current (often called "local") participant is in the life of a call. A participant can start to join a call, be in a call, have left a call, or be experiencing an error.

We can check a call’s meeting state via the call object with callObject.meetingState(). If a participant is joining a meeting, the "joining-meeting" event will be returned, for example.

Meeting state changes trigger events like "joining-meeting." We can add event listeners for those state changes with callObject.on().

State #2: participant state

Participant state monitors all participants on the call (including the current user) and the video, audio, or other media they’re sharing with everybody else.

callObject.participants() returns a set of participant objects, keyed by an ID (or "local", for the current user). Each participant object includes fields like tracks, including the participant's raw audio and video tracks and their playable states.

The events "participant-joined", "participant-left", and "participant-updated" broadcast changes to participant state. The first two are only sent when participants other than the current local participant join or leave, while the latter fires on changes like camera and microphone toggling for any participant, including local.

Now that we’ve covered the Daily call object and its states, we’re ready to look at our app.

What’s happening in the code

App.js: Creating, joining, and leaving a call

Before we get into the details of each stage of a meeting, let’s look at how we wire up our top-level event listeners. In App.js, we listen for changes to callObject.meetingState(), so we can update the UI for the local participant depending on where they’re at in their user journey: in a call, out of a call, or experiencing errors:

When a local participant leaves a meeting, we call callObject.destroy(). We do this to clean up our call object's global footprint, to open the door for our app to create another call object in the future with different create-time options.

Creating a call

When a participant clicks to start a call, they invoke the createCall() function to create a short-lived, demo-only room.

In real production code you'll want to create rooms by calling the Daily REST API from your backend server, to avoid storing API keys in your client-side JavaScript [1].

Joining a call

Once we have a room, we’ll join it by invoking the .join() method on the call object [2].

Leaving a call

When a participant clicks the “Leave” button, we’ll initiate that process by invoking the leave() method on the call object [3, 4].

Call.js and callState.js: Using state to determine the call display

We now know how different operations in a call take place, so the next step is to know how those operations affect our display. This involves keeping tabs on participant state in order to display the call's participants and their video and audio tracks.

While App.js listened to callObject.meetingState(), in Call.js we’ll listen for callObject.participantState() and update our component state accordingly [5].

Our demo app displays each participant (including the current user) as their own "tile", and also displays any screen share as its own tile independent of the participant doing the sharing.

To accomplish this, we map callObject.participantState() to the call’s component state, specifically into a set of "call items" in callState.js:

Each call item corresponds to a call participant, storing the participant’s video track, audio track, and a boolean that notes whether or not a participant is in the process of joining a call [6].

To populate the call items, we call our getCallItems() function, which loops over participant state:

We import callState in Call.js, where we invoke the getTiles() function to pass participant video and audio tracks to their respective tile components.

Now let’s take a closer look at those tiles.

Pink and yellow tiles shift colors

Tile.js: displaying each participant’s video stream

Each of our tile components contains either a <video> and/or an <audio> element. Each tag references its respective DOM element [7]. Note the autoPlay muted playsInline attribute. These are the set of attributes that will let your audio and video play automatically on Chrome, Safari, and Firefox.

Next up: give participants control over whether or not they display their videos and share their audio or screens.

Tray.js: Enable participant controls

Once again we'll use participant state to determine whether we're actively sharing audio, video, and our screen.

We’ll look specifically at callObject.participants().local, since we’re concerned about adjusting the user interface for the current, or local, user. The only event we need to listen to is "participant-updated" [8].

With our event listener handling state updates, we can wire up our buttons to handle the relevant callObject methods to control user input: .setLocalVideo, .setLocalAudio, and .startScreenShare or .stopScreenShare.

What to add next

Congratulations! If you’ve read this far, you now have an overview of your custom video chat app. To dig even deeper into the code, have a look at how the demo handles edge cases over on the Daily blog. Or, dive into our demo repository.


To see everything else the Daily APIs have to offer, grab a cup of tea and head over to for some fun evening reading.

Thanks for reading! As always, we'd love to know what you think and how we can better help you to build that next great video chat app, so please don't hesitate to reach out.

Dinaosaur in a cup that has a tea rex tea bag


[0] If you’d like to get familiar with React and come back later, check out the many great resources on DEV (like Ali Spittel’s intro), or the React docs for more on hooks.
[1] Our teammate wrote an excellent post on how to set up an instant Daily server on Glitch.
[2] Note that, because we invoke destroy() on our call object after each call ends, we need to create a new call object in order to join a room. This is not strictly necessary — you could hold onto a single call object for the lifetime of your app if you so desired, but, as we mentioned earlier, we prefer this approach to leave the door open for a future differently-configured call object.
[3] You might’ve noticed that both the join() and leave() call object operations are asynchronous, as is destroy(). To avoid undefined behavior and app errors, like leaving and destroying a call object simultaneously, it's important to prevent triggering one call object operation while another is pending. A straightforward way to do this is to use meeting state to update relevant buttons' idle states so that the user can't start an operation until it's safe, like we do in our demo app.
[4] Because destroy() is asynchronous, the DailyIframe.createCallObject() must only be invoked once destroy()'s Promise has been resolved.
[5] In the demo app, we use a reducer to update the component state.
[6] We only set isLoading to true if we’ve never received audio or video tracks for a participant.
[7] We did that so we could programmatically set their srcObject properties whenever our media tracks change (see lines 18-31 in Tile.js).
[8] You might remember that "participant-joined" and "participant-left" are only ever about other (not local) participants.

Top comments (3)

bkrajancich profile image
Brooke Krajancich

Is anyone else getting an error "Unhandled Rejection (Error): property 'url': url should be a string" when running the GitHub repo (unchanged) locally?

tushardeepak profile image
Tushar Deepak

my npm run dev is showing 'PORT' is not recognized as an internal or external command,
operable program or batch file.

can you help

nikhilkaimal profile image
Nikhil Kaimal

try "dev": "set PORT=3002 && react-scripts start" in package.json