This post will walk through the creation of a realtime chat application with Next.js and deploying it to Vercel.
You'll learn how to:
- Create a brand new Next.js application
- Adding realtime functionality with Ably
- Create a Next.js Vercel Serverless API
- Use React Functional components and React Hooks with Ably
- Host your app on Vercel
Check out a running version of the app or fork the app on GitHub.
WebSockets in Vercel with Ably
Vercel allows users to deploy Serverless Functions, which are essentially just blocks of code that provide a response to an HTTP request. However, these functions have a maximum execution timeout, which means that it is not possible to maintain a WebSocket connection this way.
This is where Ably comes in. The client can connect to an Ably Channel and send and receive messages on it to add realtime functionality to your app by managing your WebSocket connections for you.
We’ll start by going over how to build an app that uses realtime functionality. If you prefer, you can jump straight to how to use Ably with Vercel.
This is absolutely wonderful. Think serverless https://t.co/KQPuGDLPMl! https://t.co/LGoHjcrYZQ
— Guillermo Rauch (@rauchg) March 2, 2021
What are we going to build?
We’re building a realtime chat app that runs in the browser. It will be built upon the Next.js create-next-app template and contain a React component that uses Ably to send and receive messages. We'll also write a Next.js serverless function that will be used to connect to Ably.
Dependencies
To build this app, you’ll need:
- An Ably account for sending messages: Create an account with Ably for free.
- A Vercel account for hosting on production: Create an account with Vercel for free.
- Node 16.14 or greater: Install Node.
Once you have your Ably account you'll need an API key to authenticate with the Ably Service. To get an API key:
- Visit your app dashboard and click on "Create New App".
- Give the new app a name.
- Copy the Private API key once the app has been created. Keep it safe, this is how you will authenticate with the Ably service.
Vercel provides Next.js command line tools to help us. They don't need to be installed on your system as they're executed using npx
.
Scaffolding the Next.js Chat App
Start building the chat application by using create-next-app
to scaffold a new Next.js-based application. In your terminal, type:
npx create-next-app@latest
Answer the prompts as follows:
What is your project named? nextjs-chat-app
Would you like to use TypeScript? No
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? No
Would you like to use `src/` directory? No
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias? No
What import alias would you like configured? @/*
After the prompts, create-next-app
will create a folder with your project name and install the required dependencies. Change directories into your application directory:
cd nextjs-chat-app
Next, create a file called .env
in the root of the directory. This is where we'll put the project's environment variables. Add your Ably API key to the .env
file:
ABLY_API_KEY=your-ably-api-key:goes-here
Test that the application runs correctly by running the following command:
npm run dev
The Next.js dev server will spin up and you'll see an default Next.js starter app. This is what we'll build our chat app on top of.
Realtime pub/sub messaging with Ably
The chat app will use Ably for Publish/Subscribe (Pub/Sub) messaging between the users. Pub/Sub is a popular pattern used for realtime data delivery. The app will be able to send, or publish
, messages over an Ably Channel. Clients that use the app will be subscribed
to the channel and able to receive messages. We'll build a UI to create messages to be sent and to display messages as they are received.
The realtime chat app architecture
The topology of our Next.js app will look like this:
├─ .env
├─ .gitignore
├─ package-lock.json
├─ package.json
├─ README.md
|
├─── components
│ ├─ Chat.jsx
| ├─ ChatBox.jsx
│ ├─ ChatBox.module.css
|
├─── app
│ ├─ globals.css
│ ├─ layout.js
│ ├─ page.js
│ │
│ └─── api
│ └─ route.js
│
└─── public
/app/page.js
is the home page/app/api/route.js
is our Ably token authentication API/components/Chat.jsx
is the main chat host component/components/ChatBox.jsx
the component that contains the actual chat UI/components/ChatBox.module.css
contains the styles for the chat component
Let's walk through how this application is built.
Authenticating an Ably client
There are a number of ways to authenticate an Ably client. For our application, we’ll use token authentication. Token authentication uses a trusted device with an API key, in our case an API route that we’ll build using Next.js, to issue time-limited tokens to untrusted clients.
In version 13, Next.js introduced the new App Router. Built on React Server Components it supports shared layouts, nested routing, loading states, error handling, and more including API routes via its Route Handler feature. Let's build that API route.
Start by installing the Ably npm package in the root of your new app:
npm install ably
Next, create a file called ./app/api/route.js
. Add the following code:
import Ably from "ably";
// ensure Vercel doesn't cache the result of this route,
// as otherwise the token request data will eventually become outdated
// and we won't be able to authenticate on the client side
export const revalidate = 0;
export async function GET(request) {
const client = new Ably.Rest(process.env.ABLY_API_KEY);
const tokenRequestData = await client.auth.createTokenRequest({
clientId: "ably-nextjs-demo",
});
return Response.json(tokenRequestData);
}
This function uses the Ably SDK to create a tokenRequest
with your API key. The token will be used later - it allows you to keep your "real" API key safe while using it in the Next.js app. By default, this API is configured to be available on http://localhost:3000/api/. We're going to provide this URL to the Ably SDK in our client to authenticate with Ably.
Next.js Server vs Client Components
Before we start creating pages in our application, it's important to understand how Next.js renders content. The framework supports multiple rendering methods including server-side rendering (SSR), static site rendering (SSG), and client-side rendering (CSR). There are many pros and cons to each rendering method (too many to cover in this post) so if these concepts are new to you, Google’s web.dev site has a very good introduction to rendering on the web that can help you understand rendering options.
Next.js defaults to server-side rendering meaning the page HTML is generated on the server for each request and sent to the browser. This means that by default when you create a new component in your React app, you are creating a Server Component.
Server Components offer a lot of benefits, but one drawback is that they don’t have access to browser APIs. To allow you to create components that can access those APIs Next.js also includes the ability to create Client Components.
To indicate that you want a component to be a Client Component, you use React’s use client
directive at the top of your component. Doing this tells React that it should render the component on the client side.
When using Next.js however even for components marked with use client
the framework will try to render a static preview of the component on the server. This can cause problems when creating a component that attempts to instantiate an instance of an Ably client.
To avoid this, you can use Lazy Loading to defer loading of Client Components.
Building the components
Pages in Next.js
are React components, so the app/page.js
home page is the React component that contains the page layout.
Open page.js
and replace the default scaffolded content with this markup:
import Head from 'next/head';
export default function Home() {
return (
<div className="container">
<Head>
<title>Realtime Chat App with Ably, NextJS and Vercel</title>
<link rel="icon" href="https://static.ably.dev/motif-red.svg?nextjs-vercel" type="image/svg+xml" />
</Head>
<main>
<h1 className="title">Next.js Chat Demo</h1>
{/* Insert Chat Component */}
</main>
<footer>
Powered by <a href="https://vercel.com" target="_blank" rel="noopener noreferrer">Vercel</a> and <a href="https://ably.com" rel="noopener noreferrer">Ably</a>
<a href="https://github.com/ably-labs/NextJS-chat-app" className="github-corner" aria-label="View source on GitHub">
<svg width="80" height="80" viewBox="0 0 250 250" className="svg" aria-hidden="true">
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" className="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" className="octo-body"></path></svg>
</a>
</footer>
</div>
)
}
Next, in the same app
directory, locate the globals.css
file and replace its contents with the basic CSS styles we’ll use in our application which you can find in the projects GitHub repository.
Run the app. You should see this:
Next, in the root of your application create a new components
folder and within create a new React component file named Chat.jsx
. Scaffold the initial component using the code below:
'use client';
import * as Ably from 'ably';
import { AblyProvider, ChannelProvider } from 'ably/react';
export default function Chat() {
const client = new Ably.Realtime({ authUrl: '/api' });
return (
<AblyProvider client={client}>
<ChannelProvider channelName='chat-demo'></ChannelProvider>
</AblyProvider>
);
}
This component is marked with the use client
directive. It will be the boundary between SSR components and CSR components.
The component imports the Ably and Ably React paths from the Ably package and uses them to create a new instance of an Ably client, authenticating it against the API endpoint we created above and then providing that instance to an <AblyProvider>
component. <AblyProvider>
will retain the client in the application context so that any child component can access it.
We also use the <ChannelProvider>
component to define the Ably channels we want to use. For now, we will define the <ChannelProvider>
with the chat-demo
channel name without any additional options.
To use our new Chat component we’ll dynamically import it into our home page at the top of page.js
:
import Head from 'next/head';
import dynamic from 'next/dynamic';
const Chat = dynamic(() => import('../components/Chat'), {
ssr: false,
})
And add the component to the markup:
<main>
<h1 className="title">Next.js Chat Demo</h1>
<Chat />
</main>
Validate that the application works properly by running the project again. You won’t see any difference in the UI yet, but we’ll get to that in the next section.
Writing the chat component logic
The chat app logic is contained inside a new ChatBox
component. To create this start by creating two new files: ChatBox.jsx
which will hold the React component and ChatBox.module.css
, a stylesheet we’ll use to style the ChatBox component.
As we did above with globals.css
you can get the CSS needed to style the ChatBox component by copying into it the styles which you can find in the projects Github repository.
Next, open the Chatbox.jsx
file and add the import statements we'll need at the top of the file:
import React, { useEffect, useState } from 'react';
import { useChannel } from 'ably/react';
import styles from './ChatBox.module.css';
Define a ChatBox
function that is exported as a React component. Add to the function variables to store references to some of the HTML elements in the code.
export default function ChatBox() {
let inputBox = null;
let messageEnd = null;
return null;
}
Set up the state properties used in the component:
const [messageText, setMessageText] = useState("");
const [receivedMessages, setMessages] = useState([]);
const messageTextIsEmpty = messageText.trim().length === 0;
messageText
will be bound to a textarea element where messages can be typedreceiveMessages
stores the on-screen chat historymessageTextIsEmpty
is used to disable the send button when the textarea is empty
Now we'll use the useChannel
hook that we imported earlier.
useChannel
is a react-hook API for subscribing to messages from an Ably channel. You provide it with a channel name and a callback to be invoked whenever a message is received.
Both the channel instance and the Ably JavaScript SDK instance are returned from useChannel.
const { channel, ably } = useChannel("chat-demo", (message) => {
const history = receivedMessages.slice(-199);
setMessages([...history, message]);
});
Here we're computing the state that'll be drawn into the message history. We do that by slicing the last 199 messages from the receivedMessages
buffer.
Then we take the message history and combine it with the new message. This means we'll always have up to 199 messages + 1 new message, stored using the setMessages
React useState
hook.
Next, we need to handle the UI interactions by defining a few functions.
First, there's sendChatMessage
, which is responsible for publishing new messages. It uses the Ably Channel returned by the useChannel hook, clears the input, and focuses on the textarea so that users can type more messages:
const sendChatMessage = (messageText) => {
channel.publish({ name: "chat-message", data: messageText });
setMessageText("");
inputBox.focus();
}
Then handleFormSubmission
, which is triggered when the submit button is clicked and calls sendChatMessage
, along with preventing a page reload:
const handleFormSubmission = (event) => {
event.preventDefault();
sendChatMessage(messageText);
}
Finally the handleKeyPress
event is wired up to make sure that if a user presses the enter key, while there is text in the textarea, the sendChatMessage
function is triggered.
const handleKeyPress = (event) => {
if (event.charCode !== 13 || messageTextIsEmpty) {
return;
}
sendChatMessage(messageText);
event.preventDefault();
}
Next, we need to construct the UI elements to display the messages. To do this, we will map the received Ably messages into HTML span elements:
const messages = receivedMessages.map((message, index) => {
const author = message.connectionId === ably.connection.id ? "me" : "other";
return <span key={index} className={styles.message} data-author={author}>{message.data}</span>;
});
In order to keep the message box scrolled to the most recent message (the one on the bottom) we'll need to add an empty div element into the message container, which will then be scrolled into view whenever the components re-renders. This is the element that we'll add to the UI later:
<div ref={(element) => { messageEnd = element; }}></div>
We use a useEffect
hook along with scrollIntoView()
to scroll the message history to the bottom whenever the component renders.
useEffect(() => {
messageEnd.scrollIntoView({ behaviour: "smooth" });
});
Finally, we will write the React component markup with the event handlers all bound to the onChange
and onKeyPress
events in JSX.
The markup itself is just a few div elements and a form with a textarea for user input. There are two calls to the react ref
function, which allows us to capture a reference to the elements when they are rendered so that we can interact with them in JavaScript. The returned markup will look like this:
return (
<div className={styles.chatHolder}>
<div className={styles.chatText}>
{messages}
<div ref={(element) => { messageEnd = element; }}></div>
</div>
<form onSubmit={handleFormSubmission} className={styles.form}>
<textarea
ref={(element) => { inputBox = element; }}
value={messageText}
placeholder="Type a message..."
onChange={e => setMessageText(e.target.value)}
onKeyPress={handleKeyPress}
className={styles.textarea}
></textarea>
<button type="submit" className={styles.button} disabled={messageTextIsEmpty}>Send</button>
</form>
</div>
)
Add the ChatBox component to the Chat component
'use client';
import * as Ably from 'ably';
import { AblyProvider, ChannelProvider } from 'ably/react';
import ChatBox from './ChatBox.jsx';
export default function Chat() {
const client = new Ably.Realtime({ authUrl: '/api' });
return (
<AblyProvider client={client}>
<ChannelProvider channelName='chat-demo'>
<ChatBox />
</ChannelProvider>
</AblyProvider>
);
}
Swap over to your browser (make sure you restart the application server if you need to) and you should see a functioning chat application now.
Hosting on Vercel
We're using Vercel as our development server and build pipeline.
The easiest way to deploy Next.js to production is to use the Vercel platform from the creators of Next.js. Vercel is an all-in-one platform with Global CDN supporting static & Jamstack deployment and Serverless Functions.
-- The Next.js documentation
In order to deploy your new chat app to Vercel you'll need to:
- Create a GitHub account (if you don't already have one)
- Push your app to a GitHub repository
- Create a Vercel account
- Create a new Vercel app and import your app from your GitHub repository. (This will require you to authorize Vercel to use your GitHub account)
- Add your
ABLY_API_KEY
as an environment variable - Watch your app deploy
- Visit the newly created URL in your browser!
Make it your own
There are a few ways that this example could be extended:
Add message history
There is currently no chat history in this demo, you'll only see messages that come in after you join the chat. You could expand this demo by using Ably's rewind feature for up to two minutes of history for free, or with a paid account, for up to ~48 hours.
Add user names
There aren't any usernames sent with the chat messages. This demo could be extended to introduce a username input box, and to add the current username to messages as they're sent.
The demo uses the randomly generated Ably client Id as a unique identifier - which is how it can detect if it is "me" or "someone else" who sent the message.
Let us know
Hope you have enjoyed this tutorial. If you are interested in learning more about realtime in Next.js, check out these resources:
- Building a realtime SMS voting app... In the web
- Show SMS Notifications in the Browser with Next.JS, Ably, and Vercel
If this tutorial was helpful, or you're using Next.js and Ably in your project, we'd love to hear about it. Drop us a message on Twitter or email us at [email protected].