Build a real-time video chat app with React Native
Tags: reactnative, react, javascript, webdev
Published: 2021-11-29 19:21:35

---
title: Build a real-time video chat app with React Native
published: true
description: Create and join a multi-participant video call in a React Native mobile app
tags: reactnative, react, javascript, webdev
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/stppr2jbqrk5trs03fah.png
canonical_url: https://www.daily.co/blog/build-a-mobile-video-chat-app-with-dailys-react-native-javascript-library/
---

Daily’s [React Native library](https://github.com/daily-co/react-native-daily-js) allows developers to build mobile apps compatible with both Android and iOS with one codebase. It also means your web developers, who have _likely_ crossed paths with [React](https://reactjs.org/) at some point, can write code that will get compiled into native mobile code with a smaller learning curve, since React and React Native are fairly similar.

Recently on the Daily blog, we discussed how to [build your own audio call app](https://www.daily.co/blog/build-your-own-audio-only-clubhouse-clone-app-with-dailys-react-native-library/) in React Native. That tutorial specifically looked at Daily’s [Party Line demo app](https://github.com/daily-demos/party-line/tree/main/react-native), which was built to handle the use case where every call is always audio-only.


### Today's agenda

In today’s tutorial, we’ll take a look at Daily’s React Native [Playground demo app](https://github.com/daily-co/react-native-daily-js-playground), which uses more of a traditional video call format; call participants have the option to turn on and off both audio and video. 

More specifically, we’ll cover:
1. How to build a multi-participant video call in React Native with [`react-native-daily-js`](https://github.com/daily-co/react-native-daily-js)
2. How to give call participants control of their devices in the video call to toggle their local microphone and camera

---

## Who is this tutorial for?

If you are interested in building a mobile app with video calls and have some React Native (or even React) knowledge, this tutorial is for you. React Native projects can be a little more finicky to run locally than web apps because of the platform-specific setup requirements, so having some comfort with React Native is a big plus.

We won’t cover every section of the [Playground demo app](https://github.com/daily-co/react-native-daily-js-playground) code base in this tutorial since a lot of the functionality is similar to `daily-js` (web) apps, which we promise already have a bunch of [existing tutorials](https://www.daily.co/blog/tag/custom-daily/).📚 

### A note on today’s stack and React Hooks

Since this app is written in React Native, we’ll be looking at React Native code examples and React hooks from the demo codebase. We recommend familiarizing yourself with [React Hooks](https://reactjs.org/docs/hooks-intro.html) before reading on to get the most out of this tutorial.

We’ve also used [TypeScript](https://www.typescriptlang.org/) in this demo app. TypeScript isn’t specifically discussed below, but having some familiarity with it will help you review the code examples.

---

## Getting started

For anyone new to React Native app development, we’ll quickly cover some basics.

Typically, you’ll want to test on both Android and iOS mobile or tablet devices to ensure your app is working in both operating systems. To test on iOS devices, you’ll need to download [XCode](https://developer.apple.com/xcode/), which is only available to download on Mac computers. (You’ll also want to pour yourself a coffee or two while it downloads and hope you’re not on deadline. ☕)

_Note: This means you will need a Mac to access XCode for iOS development._

Android, however, can be tested with [Android Studio](https://developer.android.com/studio?gclid=CjwKCAiA7dKMBhBCEiwAO_crFAXL7AiOJQ7v-vpnt6HBCtd4Xy1h114KWPvJOES-XFGyqpkxIqaVABoCwesQAvD_BwE&gclsrc=aw.ds#downloads), which is available on a range of operating systems.

In terms of running the [Daily Playground demo app](https://github.com/daily-co/react-native-daily-js-playground) locally, thorough instructions for both Android and iOS development are included in the [repo’s README](https://github.com/daily-co/react-native-daily-js-playground/blob/main/README.md). 

_Note: You’ll need to use real devices instead of a device emulator when you’re testing the video/audio features. In terms of which OS to start with, if you don’t have a personal preference, it’s typically faster to get this app running on an Android device._

---

## App features and functionality

As mentioned, we won’t cover every part of this codebase. To start, let’s discuss the overall structure and functionality of the app so you know how to navigate it.

The `App` component is the top-level parent component. It renders either the home screen or the in-call view.

![Two app views: home screen and in-call](https://www.daily.co/blog/content/images/2021/11/android-screens.png) 

Let’s quickly review how the home screen works.

When you first land on the home screen, there’s an empty room URL text input, a “Create demo room” button, and a disabled “Join call” button. 

If you know which Daily room you want to join, you can enter the room URL in the text input and press “Join call”, which will be enabled once the input has a value.

If you do _not_ have a room URL, we’ve set up an endpoint that will create a new room for you using [Daily’s REST API](https://docs.daily.co/reference/rest-api/rooms/create-room). This endpoint is called when the “Create room” button is pressed, which calls the method `createRoom`, defined in `App`:

```javascript
// App.tsx
<Button
    type="secondary"
    onPress={createRoom}
    label={
       appState === AppState.Creating
          ? 'Creating room...'
          : 'Create demo room'
    }
/>
```

```javascript
// App.tsx
 const createRoom = () => {
   setRoomCreateError(false);
   setAppState(AppState.Creating);
   api
     .createRoom()
     .then((room) => {
       setRoomUrlFieldValue(room.url);
       setAppState(AppState.Idle);
     })
     .catch(() => {
       setRoomCreateError(true);
       setRoomUrlFieldValue(undefined);
       setAppState(AppState.Idle);
     });
 };
```

Here, we update our `appState` state value to be in a temporary “creating” state, call `api.createRoom()`, and, if it’s successful, set our `roomUrlFieldValue` value and `appState`. (Both `appState` and `roomUrlFieldValue` are component state values initialized in `App`.)

_Note: Take a look at [api.ts](https://github.com/daily-co/react-native-daily-js-playground/blob/main/DailyPlayground/src/api.ts) to see the `api.createRoom()` method._

Whether you use your own Daily room URL or one created in the app, when you press the “Join call” button, it will take the `roomUrlFieldValue`, set the `roomUrl` state value with it, and kick off creating the Daily [call object](https://docs.daily.co/call-object). 

Here we have the “Join call” button:


```javascript
// App.tsx
// “Join call” button will call startCall on press
<StartButton
   onPress={startCall}
   disabled={startButtonDisabled}
   starting={appState === AppState.Joining}
/>
```

Next, we call `startCall`:

```javascript
// App.tsx
/**
  * Join the room provided by the user or the
  * temporary room created by createRoom
 */
 const startCall = () => {
   setRoomUrl(roomUrlFieldValue);
 };
```

And lastly, a `useEffect` hook is triggered by the `roomURL` value getting updated, which creates our Daily call object (the brain of this operation!)

```javascript
// App.tsx
/**
  * Create the callObject as soon as we have a roomUrl.
  * This will trigger the call starting.
  */
 useEffect(() => {
   if (!roomUrl) {
     return;
   }
   const newCallObject = Daily.createCallObject();
   setCallObject(newCallObject);
 }, [roomUrl]);

```

The following line is where the call object is actually created:
`const newCallObject = Daily.createCallObject();`

Then, by setting that value in our component’s state, the call object instance can be referred to later:

`setCallObject(newCallObject);`

After the call object has been created, we can then actually join our room (finally! Considering we pressed the “Join call” button 😉)


```javascript
// App.tsx
 useEffect(() => {
   if (!callObject || !roomUrl) {
     return;
   }
   callObject.join({ url: roomUrl }).catch((_) => {
     // Doing nothing here since we handle fatal join errors in another way,
     // via our listener attached to the 'error' event
   });
   setAppState(AppState.Joining);
 }, [callObject, roomUrl]);
```

Here, in another `useEffect` hook in `App`, when the `callObject` and `roomUrl` state values are truthy, which they now are, we can actually [`join`](https://docs.daily.co/reference/rn-daily-js/instance-methods/join) our call by passing the `roomUrl` to our call object instance. 

This step is also where our app view will change from the home screen to the in-call view. This happens because of this line in the effect above: `setAppState(AppState.Joining);`

```javascript
// App.tsx
 const showCallPanel = [
   AppState.Joining,
   AppState.Joined,
   AppState.Error,
 ].includes(appState);

```

When `showCallPanel` — shown above — is truthy, our in-call view will render instead of the home screen:

```javascript
// App.tsx
<View style={styles.container}>
    {showCallPanel ? (
         <View style={[
             styles.callContainerBase,
             	orientation === Orientation.Landscape
             		? styles.callContainerLandscape
             		: null,
         ]}>
             <CallPanel roomUrl={roomUrl || ''} />
             <Tray
                onClickLeaveCall={leaveCall}
                disabled={!enableCallButtons}
             />
    	</View>
	) : (
	… //home screen
    )
...
```

We’ll leave it at that for the home screen and focus on the `CallPanel` component — our in-call view — for the rest of this tutorial. If you have any questions about this section, please [reach out](https://www.daily.co/contact/support)! We’re happy to help. 🙌

---

## Displaying video tiles in your Daily React Native app

Let’s start by familiarizing ourselves with what our in-call app UI is supposed to look like:

![In-call UI featuring a developer talking to herself](https://www.daily.co/blog/content/images/2021/11/CleanShot-2021-11-25-at-15.44.03.png) 

We have the local participant’s camera feed at the top left corner, the room URL and a button to copy it to your clipboard in the middle of the screen, and our tray at the bottom. If anyone is screen sharing, they’ll also be included as a small thumbnail at the top.

_Note: Screen sharing can’t be initiated in this app but call participants can join the room from any platform, including a web app using `daily-js`, where [screen sharing](https://docs.daily.co/reference/daily-js/instance-methods/start-screen-share) is permitted._

The tray (i.e. the `Tray` component) has buttons to toggle the local participant’s audio, video, and to leave the call.

When more participants join, their videos are shown in the middle of the screen, replacing the room URL information.

### Iterating over our participant list

Now that we know what we’re talking about, let’s jump right to where we’re actually creating our participant videos with `react-native-daily-js`. 

In `CallPanel.tsx`, we render an array called `largeTiles`, which represents the remote participants.


```javascript
// CallPanel.tsx
<ScrollView
     alwaysBounceVertical={false}
     alwaysBounceHorizontal={false}
     horizontal={orientation === Orientation.Landscape}
 >
     <View
        style={[
            styles.largeTilesContainerInnerBase,
                orientation === Orientation.Portrait
                 ? styles.largeTilesContainerInnerPortrait
                 : styles.largeTilesContainerInnerLandscape,
         ]}
      >
         {largeTiles} // <- our remote participants
      </View>
 </ScrollView>
```

_Note: We’ve put this in a [`ScrollView`](https://reactnative.dev/docs/scrollview) but you may prefer a [`FlatList`](https://reactnative.dev/docs/flatlist) component if you know you will be having larger calls. (A `FlatList` will only render the visible tiles, which should help with performance. It’s less of a concern in 1-on-1 video calls.)_

Our `largeTiles` (remote participants) and `thumbnailTiles` (the local participant or screen sharer) are determined by the same [memoized](https://reactjs.org/docs/hooks-reference.html#usememo) function. The tiles in `largeTiles` can be either full size or half size depending on the number of participants.

![Full and half size remote participant tiles](https://www.daily.co/blog/content/images/2021/11/android-multi.png) 

```javascript
// CallPanel.tsx
 /**
  * Get lists of large tiles and thumbnail tiles to render.
  */
 const [largeTiles, thumbnailTiles] = useMemo(() => {
   let larges: JSX.Element[] = [];
   let thumbnails: JSX.Element[] = [];
   Object.entries(callState.callItems).forEach(([id, callItem]) => {
     let tileType: TileType;
     if (isScreenShare(id)) {
       tileType = TileType.Full;
     } else if (isLocal(id) || containsScreenShare(callState.callItems)) {
       tileType = TileType.Thumbnail;
     } else if (participantCount(callState.callItems) <= 3) {
       tileType = TileType.Full;
     } else {
       tileType = TileType.Half;
     }
     const tile = (
       <Tile
         key={id}
         videoTrackState={callItem.videoTrackState}
         audioTrackState={callItem.audioTrackState}
         mirror={usingFrontCamera && isLocal(id)}
         type={tileType}
         disableAudioIndicators={isScreenShare(id)}
         onPress={
           isLocal(id)
             ? flipCamera
             : () => {
                 sendHello(id);
               }
         }
       />
     );
     if (tileType === TileType.Thumbnail) {
       thumbnails.push(tile);
     } else {
       larges.push(tile);
     }
   });
   return [larges, thumbnails];
 }, [callState.callItems, flipCamera, sendHello, usingFrontCamera]);

```

Let’s step through this function:
- We declare two arrays that we’ll be updating in this function: `larges` and `thumbnails`
- We get an array of our call participants (`Object.entries(callState.callItems)`) and do the following for each (or `forEach`, if you will):
  - _Note: The `tileType` can be `TileType.Full`, `TileType.Half`, or  `TileType.Thumbnail`.  The latter is the local participant, and the first two options are for remote participants (our `largeTiles`).
  - If the “participant” is actually a screen share, we make it a full size tile
  - If the participant is local or currently sharing their screen, we make them a thumbnail
  - If the call has 3 or less participants total, remote participants will have full size tiles; otherwise, they’ll have half size tiles.
  - We then render a `Tile` component for each participant and update our `larges` and `thumbnails` arrays

Okay, we’ve come pretty far but we still need to render our actual video and audio for the participants, so bear with us!

### Rendering participant media
The most important part of the `Tile` component is the `mediaComponent`, a memoized instance of the `DailyMediaView` component imported from `react-native-daily-js`:


```javascript
// Tile.tsx
import {
   DailyMediaView,
} from '@daily-co/react-native-daily-js';
... 
const mediaComponent = useMemo(() => {
   return (
     <DailyMediaView
       videoTrack={videoTrack}
       audioTrack={audioTrack}
       mirror={props.mirror}
       zOrder={props.type === TileType.Thumbnail ? 1 : 0}
       style={styles.media}
       objectFit="cover"
     />
   );
 }, [videoTrack, audioTrack, props.mirror, props.type]);
```

The `videoTrack` and `audioTrack` are props passed to `Tile` from `CallPanel` but are actually set in `callState.ts`:

```javascript
// callState.ts
function getCallItems(participants: { [id: string]: DailyParticipant }) {
 // Ensure we *always* have a local participant
 let callItems = { ...initialCallState.callItems }; 
 for (const [id, participant] of Object.entries(participants)) {
   callItems[id] = {
     videoTrackState: participant.tracks.video,
     audioTrackState: participant.tracks.audio,
   };
   if (shouldIncludeScreenCallItem(participant)) {
     callItems[id + '-screen'] = {
       videoTrackState: participant.tracks.screenVideo,
       audioTrackState: participant.tracks.screenAudio,
     };
   }
 }
 return callItems;
}

```

We’re jumping around here a bit but the important thing to understand is that our Daily `callObject` provides our participant information (see: [`callObject.participants()`](https://docs.daily.co/reference/rn-daily-js/instance-methods/participants)) and our participant information contains their media (video/audio) tracks. We can then pass those tracks to the `DailyMediaView` component to actually play those tracks in the app. 

Jumping back to the `Tile` component, we get the `videoTrack` and `audioTrack` values from the `videoTrackState` and `audioTrackState` props.


```javascript
// Tile.tsx
 const videoTrack = useMemo(() => {
   return props.videoTrackState
      && props.videoTrackState.state === 'playable'
     ? props.videoTrackState.track!
     : null;
 }, [props.videoTrackState]);
 
 const audioTrack = useMemo(() => {
   return props.audioTrackState && props.audioTrackState.state === 'playable'
     ? props.audioTrackState.track!
     : null;
 }, [props.audioTrackState]);
```

This means we use the tracks from the individual participant information if they’re available, and otherwise set that corresponding props to `null`. Both are valid types for the `DailyMediaView` `videoTrack` and `audioTrack` props.

`Tile` also has an overlay with the audio and camera muted icons when they apply (i.e. when there’s no track to play), but we won’t review that code here. Again, [let us know](https://www.daily.co/contact/support) if you have any questions. 🙏

![Tile icon overlay](https://dailyco.ghost.io/4a66d8b2-c8c1-4296-9824-7cf2eac74085) 

---

## Controlling your local devices in-call

As a final note, let’s see how our `Tray` component interacts with the Daily call object. As a reminder, it’s rendered in `App.tsx` at the same time the `CallPanel` component is rendered.

![Tray on Android](https://www.daily.co/blog/content/images/2021/11/android-tray.png)

As mentioned, the tray lets up control our local camera and microphone, as well as leave the current call to return to the home screen. 

To toggle our local camera, we can call [`setLocalAudio`](https://docs.daily.co/reference/rn-daily-js/instance-methods/set-local-video) on the call object instance.


```javascript
// Tray.tsx
 const toggleCamera = useCallback(() => {
   callObject?.setLocalVideo(isCameraMuted);
 }, [callObject, isCameraMuted]);

```

Similarly, we can toggle our microphone on or off with [`setLocalAudio`](https://docs.daily.co/reference/rn-daily-js/instance-methods/set-local-audio). 

```javascript
// Tray.tsx
 const toggleMic = useCallback(() => {
   callObject?.setLocalAudio(isMicMuted);
 }, [callObject, isMicMuted]);
```

Lastly, pressing the “Leave” button will call the `leaveCall` function call, a prop passed from `App`. 

```javascript
// App.tsx
/**
  * Leave the current call.
  * If we're in the error state (AppState.Error),
  * we've already "left", so just
  * clean up our state.
  */
 const leaveCall = useCallback(() => {
   if (!callObject) {
     return;
   }
   if (appState === AppState.Error) {
     callObject.destroy().then(() => {
       setRoomUrl(undefined);
       setRoomUrlFieldValue(undefined);
       setCallObject(null);
       setAppState(AppState.Idle);
     });
   } else {
     setAppState(AppState.Leaving);
     callObject.leave();
   }
 }, [callObject, appState]);

```

Here, we’re [`destroy`](https://docs.daily.co/reference/rn-daily-js/instance-methods/destroy)ing our call object instance and resetting the state in `App` to get back to our initial values.

---

## Resources

We hope this helps you navigate building your own video call apps with Daily’s React Native library. We covered the most important aspects of our [Playground app](https://github.com/daily-co/react-native-daily-js-playground/blob/main/DailyPlayground/src/api.ts) but we’re always happy to answer any questions you may have! 😁

If you’re looking to learn more about building with Daily’s React Native library, check out our beloved [docs](https://docs.daily.co/reference/rn-daily-js) or read our previous tutorial on building a Clubhouse clone app. 📱