With the launch of our new docs site, we’ve been spending a lot of time in Next.js. We even got a little meta and embedded a Daily Prebuilt demo, built on Next, into the docs site, also built on Next.
The demo lets readers quickly test out Daily calls, and get a sense of what Daily Prebuilt would look like embedded in their own app, right on the docs site. Our docs use Next API routes to create temporary Daily rooms dynamically server-side.
Since our docs codebase isn’t currently public, this post uses our /examples/prebuilt/basic-embed
repository as a template to show how you can do the same in any Next app. We’ll cover:
- Setting up the repository locally
- Using Next API routes to create Daily rooms dynamically server-side
- Creating a Daily callframe and joining a call once we have a room
You’ll need a Daily account if you don’t have one already.
Skip to API routes if you already have a Next project that you want to add video chat to, or if you’d rather run create-next-app
to start a new app from scratch.
Set up the demo repository
Clone the /examples
repo, and cd examples/prebuilt/basic-embed
.
Create an .env
based on .env.example
, adding your Daily domain (you set this up when you created an account) and API key (you can find this in the "Developers" tab in the Daily dashboard):
DAILY_DOMAIN="your-domain"
DAILY_API_KEY="Daily API Key"
Once you’ve added your own values, run the following from inside /basic-embed
to install dependencies and start the server:
yarn
yarn workspace @prebuilt/basic-embed dev
You should now be able to click "Create room and start" and jump into a Daily Prebuilt call:
Let’s look at how that all works.
Use API routes to create Daily video rooms dynamically server-side
Our /pages
directory is where most of the fun happens. Next pages are React components. They’re associated with routes based on their file names, and come with some other neat built-in features.
For example, files inside pages/api
are treated like API endpoints. These are Next API routes. In development, they are served by your dev servers, but in prod in our demo app they’ll get converted into Vercel functions, technically making them serverless.
In our app, we use a Next API route to create Daily rooms:
// pages/api/room/index.js
export default async function handler(req, res) {
if (req.method === 'POST') {
const options = {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.DAILY_API_KEY}`,
},
body: JSON.stringify({
properties: {
enable_prejoin_ui: true,
enable_network_ui: true,
enable_screenshare: true,
enable_chat: true,
exp: Math.round(Date.now() / 1000) + 300,
eject_at_room_exp: true,
},
}),
};
const dailyRes = await fetch(
`${process.env.DAILY_REST_DOMAIN}/rooms`,
options
);
const response = await dailyRes.json();
if (response.error) {
return res.status(500).json(response.error);
}
return res.status(200).json(response);
}
return res.status(500);
}
All requests to /room
are handled here, and we’re specifically adding a case to handle a POST request. The request references both the Daily API key and base REST domain in the .env.
We send this request in the <CreateRoomButton />
component. This component is a button that onClick creates a room:
// components/CreateRoom.js
return (
<Button onClick={createRoom} disabled={isValidRoom}>
Create room and start
</Button>
);
createRoom()
sends a request to the Next /api/room
endpoint, which makes the Daily endpoint POST request in api/room/index
described above:
// components/CreateRoom.js
const createRoom = async () => {
try {
const res = await fetch('/api/room', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
// Abridged snippet
};
When that request resolves, it returns the Daily response object, including a url
value. createRoom()
sets the room
value stored in local state to the response object’s url
:
// components/CreateRoom.js
const resJson = await res.json();
setRoom(resJson.url);
Now that we have a room, we’re ready for a callframe.
Create a Daily callframe and join a call
Our <Call />
component not only renders <CreateRoom />
, but also initializes the callframe with a useEffect
hook:
// components/Call.js
useEffect(() => {
if (callFrame) return;
createAndJoinCall();
}, [callFrame, createAndJoinCall]);
The hook calls createAndJoinCall()
, a function that:
- Creates a new Daily callframe, embedding it in the ref we identified
<div ref={callRef} className="call" />
and passing along some properties we stored in theCALL_OPTIONS
constant - Joins the Daily room using the
room
value stored in local state- Listens for the
'left-meeting'
event so it can reset app state when the local participant leaves the call
- Listens for the
// components/Call.js
const createAndJoinCall = useCallback(() => {
const newCallFrame = DailyIframe.createFrame(
callRef?.current,
CALL_OPTIONS
);
setCallFrame(newCallFrame);
newCallFrame.join({ url: room });
const leaveCall = () => {
setRoom(null);
setCallFrame(null);
callFrame.destroy();
};
newCallFrame.on('left-meeting', leaveCall);
}, [room, setCallFrame]);
createAndJoinCall()
is invoked whether a room is created dynamically in real-time, as we walked through in the <CreateRoom />
component, or a room is submitted through the input rendered in <Home />
:
// components/Home.js
<Field label="Or enter room to join">
<TextInput
ref={roomRef}
type="text"
placeholder="Enter room URL..."
pattern="^(https:\/\/)?[\w.-]+(\.(daily\.(co)))+[\/\/]+[\w.-]+$"
onChange={checkValidity}
/>
</Field>
The input calls checkValidity()
as its values change. This function makes sure that the entered text is a valid Daily room URL based on the pattern
value, and sets the local state value isValidRoom
to true
if so:
// components/Home.js
const checkValidity = useCallback(
(e) => {
if (e?.target?.checkValidity()) {
setIsValidRoom(true);
}
},
[isValidRoom]
);
This enables the "Join room" button:
// components/Home.js
<Button onClick={joinCall} disabled={!isValidRoom}>
Join room
</Button>
Clicking the button calls joinCall()
, which sets the room
value stored in local state to the input:
// components/Home.js
const joinCall = useCallback(() => {
const roomUrl = roomRef?.current?.value;
setRoom(roomUrl);
}, [roomRef]);
The room
value in local state triggers the callframe creation in <Call />
in the same way it did when we created a room dynamically. In both cases a room
value also instructs index.js
to display the <Call />
instead of the <Home />
component, according to this ternary statement:
// pages/index.js
<main>
{room ? (
<Call
room={room}
expiry={expiry}
setRoom={setRoom}
setCallFrame={setCallFrame}
callFrame={callFrame}
/>
) : (
<Home
setRoom={setRoom}
setExpiry={setExpiry}
isConfigured={isConfigured}
/>
)}
</main>
thank u, Next.js
That’s the core of the app! There are a few other tangential things in the codebase that we didn’t get into, like the <ExpiryTimer /
>component and how we put [
getStaticProps()`](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation) to work checking for env variables, but we welcome you to explore those things yourself and ping us with questions. Or, if you’d rather build your own video chat interface with Next.js, check out our post using Next with the Daily call object.
Top comments (0)