Skip to main content

Chat App

The counter app in the previous section came together fast but it was also quite simple. Let's build something that feels more real: a chat app.

Data Modeling

We can model a chat app as a list of messages with author and body fields.

message
author: string
body: string

In your hello convex app, let's start by creating a schema for our messages table in convex/schema.ts:

convex/schema.ts
import { defineSchema, defineTable, s } from "convex/schema";

export default defineSchema({
messages: defineTable({
author: s.string(),
body: s.string(),
}),
});

A Convex schema describes the document type in each of your tables. While Convex can be used without a schema, like we did in the counter app, adding a schema will give you additional type safety throughout your app.

Here we use defineTable to define a single messages table. Within the table, we use the schema builder, s to define the type of each field in our documents.

Now that we have defined a schema, update your generated code with

npx convex codegen

This will update the type of db within query and mutation functions to be specific to our schema. It also creates a Document type in convex/_generated/dataModel.ts. This type is parameterized over the table name and gives us the type of documents in each table:

Document Type

Here we can see that Document<"message"> is a TypeScript type with the body and author fields that we defined, plus the system-defined _id and _creationTime fields. We can use this type both in our Convex query and mutation functions as well as our React components.

Armed with this data model we can write the Convex functions to post messages and fetch the list of messages. Let's create a file called sendMessage.ts in the convex directory and give it the following contents:

convex/sendMessage.ts
import { mutation } from "./_generated/server";

// Send a chat message.
export default mutation(({ db }, body: string, author: string) => {
const message = { body, author };
db.insert("messages", message);
});

This should look pretty similar to the functions in our counter app. The function takes a body and author, combines these in to an object called message, then writes that message to the database in the messages table.

Querying the data is also simple. Create a file called listMessages.ts containing the following:

convex/listMessages.ts
import { Document } from "./_generated/dataModel";
import { query } from "./_generated/server";

export default query(async ({ db }): Promise<Document<"messages">[]> => {
return await db.table("messages").collect();
});

This function fetches the entire set of messages in the messages table.

That's all we need to do as far as interacting with the chat data.

Now go ahead and deploy these functions:

npx convex push

This registered our data access functions but hasn't actually created any tables. These will be created dynamically when first accessed.

Messages and Subscriptions

Now that the two Convex functions have been deployed it's extremely easy to throw together a basic chat app. There are two interesting things our app needs to do: post chat messages and display chat messages.

Posting chats looks a lot like updating the counter in our previous app. We use our the useMutation hook generated specifically for our app to get a typed mutation function,

const sendMessage = useMutation("sendMessage");

then we call it inside an event handler to call sendMessage with the given arguments.

await sendMessage(newMessageText, randomName);

Listing chat messages is a little more interesting because it uses the magic of Convex subscriptions:

const messages = useQuery("listMessages") || [];

This code watches the listMessages Convex function and triggers a rerender any time a new chat message is sent.

You can now open your editor and build a simple UI that renders chat messages from the messages state variable, or you can just use the code that's already in the 1-convex-chat directory in the Convex Demos repo, in which case you'll need to create a new deployment by running npx convex init.

Here it is if you want to simply copy and paste our version:

src/App.tsx
import { useState, FormEvent } from "react";
import { useMutation, useQuery } from "../convex/_generated/react";
import { Document } from "../convex/_generated/dataModel";

const randomName = "User " + Math.floor(Math.random() * 10000);

// Render a chat message.
function MessageView(props: { message: Document<"messages"> }) {
const message = props.message;
return (
<div>
<strong>{message.author}:</strong> {message.body}
</div>
);
}

export default function App() {
// Dynamically update `messages` in response to the output of
// `listMessages.ts`.
const messages = useQuery("listMessages") || [];

// Run `sendMessage.ts` as a mutation to record a chat message when
// `handleSendMessage` triggered.
const [newMessageText, setNewMessageText] = useState("");
const sendMessage = useMutation("sendMessage");
async function handleSendMessage(event: FormEvent) {
event.preventDefault();
setNewMessageText(""); // reset text entry box
await sendMessage(newMessageText, randomName);
}
return (
<main className="py-4">
<h1 className="text-center">Convex Chat</h1>
<p className="text-center">
<span className="badge bg-dark">{randomName}</span>
</p>
<ul className="list-group shadow-sm my-3">
{messages.slice(-10).map((message: any) => (
<li
key={message._id}
className="list-group-item d-flex justify-content-between"
>
<MessageView message={message} />
<div className="ml-auto text-secondary text-nowrap">
{new Date(message._creationTime).toLocaleTimeString()}
</div>
</li>
))}
</ul>
<form
onSubmit={handleSendMessage}
className="d-flex justify-content-center"
>
<input
value={newMessageText}
onChange={event => setNewMessageText(event.target.value)}
className="form-control w-50"
placeholder="Write a message…"
/>
<input
type="submit"
value="Send"
className="ms-2 btn btn-primary"
disabled={!newMessageText}
/>
</form>
</main>
);
}

Let's get chatting

We're done with the hard work now. Run the UI locally with npm run dev then navigate your browser to localhost:3000 to try it out:

Chat App Single-User Screenshot

This looks like it's working, but we don't actually know this is a real distributed application. To double-check go open up another browser window at localhost:3000 and start chatting:

Chat App Multi-User Screenshot

The chats from one window will dynamically show up in all chat windows. The frontend happens to be running locally for now but will work exactly the same if you host the frontend online. Hopefully this is the easiest multi-user app you've ever had to build!

note

If you go refresh your deployment dashboard, you'll see lots more functions, tables, and data in there!