Build a real time video chat app with Next.js and Daily
Tags: nextjs, react, video, javascript
Published: 2021-06-18 21:50:46

We built one of our first [Daily demos](https://github.com/daily-demos/call-object-react?utm_source=dev&utm_campaign=dev-next-js-and-daily) with [React](https://www.daily.co/blog/building-a-custom-video-chat-app-with-react/?utm_source=dev&utm_campaign=dev-next-js-and-daily), because we like working with the framework. We’re not alone. More developers expressed interest in learning React than in picking up any other web framework in the [2020 Stack Overflow Developer Survey](https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-web-frameworks-wanted2). 

Meta frameworks for React like [Next.js](https://nextjs.org/) are also gaining traction, so we built a [basic video call demo app](https://github.com/daily-demos/examples/tree/main/dailyjs/basic-call?utm_source=dev&utm_campaign=dev-next-js-and-daily) using Next.js and the [Daily call object](https://docs.daily.co/docs/build-a-custom-video-chat-interface?utm_source=dev&utm_campaign=dev-next-js-and-daily).

![Screenshot of a video chat app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3gqpldn0ja5f12i9a0b2.png)

The demo draws inspiration from the new [Daily Prebuilt](https://www.daily.co/blog/announcing-the-new-daily-prebuilt-for-developers/?utm_source=dev&utm_campaign=dev-next-js-and-daily) (We’ll eventually open source Daily Prebuilt’s components, stay tuned!), using shared [contexts](https://reactjs.org/docs/context.html) and [custom hooks](https://reactjs.org/docs/hooks-custom.html) that we hope help get your own apps up and running ASAP. Dive right into [the repository](https://github.com/daily-demos/examples?utm_source=dev&utm_campaign=dev-next-js-and-daily) or read on for a sneak peek at some of the most foundational pieces, like the core call loop (shared contexts and hooks) and generating meeting tokens. 

## Run the demo locally 
You can find our basic Next.js and Daily video chat demo in our ✨ new ✨ [`daily-demos/examples` repository](https://github.com/daily-demos/examples/?utm_source=dev&utm_campaign=dev-next-js-and-daily). This is a living repo. It’ll grow and evolve as Daily does and as we receive feedback. Poke around and you might notice a few other demos in progress. To hop right into the basic Next.js and Daily app:

1. Fork and clone the [repository](https://github.com/daily-demos/examples/?utm_source=dev&utm_campaign=dev-next-js-and-daily)
2. `cd examples/dailyjs/basic-call`
3.  Set your `DAILY_API_KEY` and `DAILY_DOMAIN` environment variables (see [env.example](https://github.com/daily-demos/examples/blob/main/dailyjs/basic-call/env.example?utm_source=dev&utm_campaign=dev-next-js-and-daily)) 
4. `yarn`
5. `yarn workspace @dailyjs/basic-call dev`

## The core call loop: shared contexts and hooks
As you’re probably well aware in the year 2021, lots of things can happen on video calls. Participants join and leave, mute and unmute their devices, not to mention the funny things networks can decide to do. Application state can get unwieldy quickly, so we make use of the [Context API](https://reactjs.org/docs/context.html) to avoid passing ever-changing props to all the different components that need to know about the many states. 

Six contexts make up what we refer to as our call loop. They handle four different sets of state: devices, tracks, participants, and call state, in addition to a waiting room experience and the overall user interface. 

```js
// pages/index.js

  return (
    <UIStateProvider>
      <CallProvider domain={domain} room={roomName} token={token}>
        <ParticipantsProvider>
          <TracksProvider>
            <MediaDeviceProvider>
              <WaitingRoomProvider>
                <App />
              </WaitingRoomProvider>
            </MediaDeviceProvider>
          </TracksProvider>
        </ParticipantsProvider>
      </CallProvider>
    </UIStateProvider>
  );
```
Some of the contexts also make use of [custom hooks](https://reactjs.org/docs/hooks-custom.html) that abstract some complexity, depending on the, well, context.

With that pun out of the way, let’s dive into each of the contexts except for `<WaitingRoomProvider>`, You’ll have to...wait for a post on that one. 

Okay, really, we’re ready now. 

### Managing devices 
The [`<MediaDeviceProvider>`](https://github.com/daily-demos/examples/blob/main/dailyjs/shared/contexts/MediaDeviceProvider.js?utm_source=dev&utm_campaign=dev-next-js-and-daily) grants the entire app access to the cams and mics used during the call. 

```js
// MediaDeviceProvider.js

return (
   <MediaDeviceContext.Provider
     value={{
       cams,
       mics,
       speakers,
       camError,
       micError,
       currentDevices,
       deviceState,
       setMicDevice,
       setCamDevice,
       setSpeakersDevice,
     }}
   >
     {children}
   </MediaDeviceContext.Provider>
 );
```
`<MediaDeviceProvider>` relies on a [`useDevices`](https://github.com/daily-demos/examples/blob/be5d3decfc0ae2e871506430d81561fa0a8ff9c4/dailyjs/shared/hooks/useDevices.js?utm_source=dev&utm_campaign=dev-next-js-and-daily) hook to listen for changes to the call object to make sure the app has an up to date list of the devices on the call and each device’s state.

```js
// useDevices.js

const updateDeviceState = useCallback(async () => {

   try {
     const { devices } = await callObject.enumerateDevices();

     const { camera, mic, speaker } = await callObject.getInputDevices();

     const [defaultCam, ...videoDevices] = devices.filter(
       (d) => d.kind === 'videoinput' && d.deviceId !== ''
     );
     setCams(
       [
         defaultCam,
         ...videoDevices.sort((a, b) => sortByKey(a, b, 'label', false)),
       ].filter(Boolean)
     );
     const [defaultMic, ...micDevices] = devices.filter(
       (d) => d.kind === 'audioinput' && d.deviceId !== ''
     );
     setMics(
       [
         defaultMic,
         ...micDevices.sort((a, b) => sortByKey(a, b, 'label', false)),
       ].filter(Boolean)
     );
     const [defaultSpeaker, ...speakerDevices] = devices.filter(
       (d) => d.kind === 'audiooutput' && d.deviceId !== ''
     );
     setSpeakers(
       [
         defaultSpeaker,
         ...speakerDevices.sort((a, b) => sortByKey(a, b, 'label', false)),
       ].filter(Boolean)
     );

     setCurrentDevices({
       camera,
       mic,
       speaker,
     });

   } catch (e) {
     setDeviceState(DEVICE_STATE_NOT_SUPPORTED);
   }
 }, [callObject]);

```
`useDevices` also handles device errors, like if a cam or mic is blocked, and updates a device’s state when something changes for the participant using the device, like if their tracks change.  

### Keeping track of tracks 
Different devices share different kinds of tracks. A microphone shares an `audio` type track; a camera shares `video`. Each track contains its own state: playable, loading, off, etc. [`<TracksProvider>`](https://github.com/daily-demos/examples/blob/main/dailyjs/shared/contexts/TracksProvider.js?utm_source=dev&utm_campaign=dev-next-js-and-daily) simplifies keeping track of all those tracks as the number of call participants grows. This context listens for changes in track state and dispatches updates. One type of change, for example, could be when a participant’s tracks start or stop.

```js
// TracksProvider.js

export const TracksProvider = ({ children }) => {
 const { callObject } = useCallState();
 const [state, dispatch] = useReducer(tracksReducer, initialTracksState);

 useEffect(() => {
   if (!callObject) return false;

   const handleTrackStarted = ({ participant, track }) => {
     dispatch({
       type: TRACK_STARTED,
       participant,
       track,
     });
   };
   const handleTrackStopped = ({ participant, track }) => {
     if (participant) {
       dispatch({
         type: TRACK_STOPPED,
         participant,
         track,
       });
     }
   };

   /** Other things happen here **/

   callObject.on('track-started', handleTrackStarted);
   callObject.on('track-stopped', handleTrackStopped);
   }, [callObject];
``` 

### Handling participants

[`<ParticipantsProvider>`](https://github.com/daily-demos/examples/blob/main/dailyjs/shared/contexts/ParticipantsProvider.js?utm_source=dev&utm_campaign=dev-next-js-and-daily) makes sure any and all participant updates are available across the app. It listens for [participant events](https://docs.daily.co/reference#participant-events?utm_source=dev&utm_campaign=dev-next-js-and-daily): 

```js
// ParticipantsProvider.js

 useEffect(() => {
   if (!callObject) return false;

   const events = [
     'joined-meeting',
     'participant-joined',
     'participant-updated',
     'participant-left',
   ];

   // Listen for changes in state
   events.forEach((event) => callObject.on(event, handleNewParticipantsState));

   // Stop listening for changes in state
   return () =>
     events.forEach((event) =>
       callObject.off(event, handleNewParticipantsState)
     );
 }, [callObject, handleNewParticipantsState]);
```

And dispatches state updates depending on the event:

```js
// ParticipantsProvider.js

const handleNewParticipantsState = useCallback(
   (event = null) => {
     switch (event?.action) {
       case 'participant-joined':
         dispatch({
           type: PARTICIPANT_JOINED,
           participant: event.participant,
         });
         break;
       case 'participant-updated':
         dispatch({
           type: PARTICIPANT_UPDATED,
           participant: event.participant,
         });
         break;
       case 'participant-left':
         dispatch({
           type: PARTICIPANT_LEFT,
           participant: event.participant,
         });
         break;
       default:
         break;
     }
   },
   [dispatch]
 );
``` 

`<ParticipantsProvider>` also calls on [use-deep-compare](https://www.npmjs.com/package/use-deep-compare) to memoize expensive calculations, like all of the participants on the call: 

```js
// ParticipantsProvider.js

const allParticipants = useDeepCompareMemo(
   () => Object.values(state.participants),
   [state?.participants]
 );
```
### Managing room and call state 
`<CallProvider>` handles configuration and state for the room where the call happens, where all those devices, participants, and tracks interact. 

[`<CallProvider>`](https://github.com/daily-demos/examples/blob/main/dailyjs/shared/contexts/CallProvider.js?utm_source=dev&utm_campaign=dev-next-js-and-daily) imports the abstraction hook [`useCallMachine`](https://github.com/daily-demos/examples/blob/main/dailyjs/shared/contexts/useCallMachine.js?utm_source=dev&utm_campaign=dev-next-js-and-daily) to manage call state. 

```js
// CallProvider.js

 const { daily, leave, join, state } = useCallMachine({
   domain,
   room,
   token,
 });
```

`useCallMachine` listens for changes in call access, for example, and updates overall call state accordingly: 

```js
// useCallMachine.js

useEffect(() => {
   if (!daily) return false;

   daily.on('access-state-updated', handleAccessStateUpdated);
   return () => daily.off('access-state-updated', handleAccessStateUpdated);
 }, [daily, handleAccessStateUpdated]);

// Other things happen here

 const handleAccessStateUpdated = useCallback(
   async ({ access }) => {

     if (
       [CALL_STATE_ENDED, CALL_STATE_AWAITING_ARGS, CALL_STATE_READY].includes(
         state
       )
     ) {
       return;
     }

     if (
       access === ACCESS_STATE_UNKNOWN ||
       access?.level === ACCESS_STATE_NONE
     ) {
       setState(CALL_STATE_NOT_ALLOWED);
       return;
     }

     const meetingState = daily.meetingState();
     if (
       access?.level === ACCESS_STATE_LOBBY &&
       meetingState === MEETING_STATE_JOINED
     ) {
       return;
     }
     join();
   },
   [daily, state, join]
 );
```

`<CallProvider>` then uses that information, to do things like verify a participant’s access to a room, and whether or not they’re permitted to join the call: 

```js
// CallProvider.js

useEffect(() => {
   if (!daily) return;

   const { access } = daily.accessState();
   if (access === ACCESS_STATE_UNKNOWN) return;

   const requiresPermission = access?.level === ACCESS_STATE_LOBBY;
   setPreJoinNonAuthorized(requiresPermission && !token);
 }, [state, daily, token]);
```

If the participant requires permission to join, and they’re not joining with a token, then the participant will not be allowed into the call.

## Generating Daily meeting tokens with Next.js 

[Meeting tokens](https://docs.daily.co/reference#meeting-tokens?utm_source=dev&utm_campaign=dev-next-js-and-daily) control room access and session configuration on a per-user basis. They’re also a great use case for [Next API routes](https://nextjs.org/docs/api-routes/introduction). 

API routes let us query endpoints directly within our app, so we don’t have to maintain a separate server. We call the Daily `/meeting-tokens` endpoint in `/pages/api/token.js`: 

```js
// pages/api/token.js

export default async function handler(req, res) {
 const { roomName, isOwner } = req.body;

 if (req.method === 'POST' && roomName) {

   const options = {
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
       Authorization: `Bearer ${process.env.DAILY_API_KEY}`,
     },
     body: JSON.stringify({
       properties: { room_name: roomName, is_owner: isOwner },
     }),
   };

   const dailyRes = await fetch(
     `${process.env.DAILY_REST_DOMAIN}/meeting-tokens`,
     options
   );

   const { token, error } = await dailyRes.json();

   if (error) {
     return res.status(500).json({ error });
   }

   return res.status(200).json({ token, domain: process.env.DAILY_DOMAIN });
 }

 return res.status(500);
}
```

In `index.js`, we fetch the endpoint:  

```js
// pages/index.js

const res = await fetch('/api/token', {
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
     },
     body: JSON.stringify({ roomName: room, isOwner }),
   });
   const resJson = await res.json();
```

## What’s Next.js? 

Please [fork, clone](https://github.com/daily-demos/examples?utm_source=dev&utm_campaign=dev-next-js-and-daily), and hack away! There are lots of ways you could start building on top of this demo: adding custom user authentication, [building a chat component](https://www.daily.co/blog/three-ways-to-add-chat-to-your-video-calls-with-the-daily-api/?utm_source=dev&utm_campaign=dev-next-js-and-daily), or pretty much anything that springs to mind. 

We’d appreciate [hearing what you think](https://www.daily.co/contact?utm_source=dev&utm_campaign=dev-next-js-and-daily) about the demo, especially how we could improve it. We’re also curious about other framework and meta-framework specific sample code that you’d find useful. 

If you’re hoping for more Daily and Next.js sample code, we’ve got you covered. Come back soon!