How it Works
Once you've got a Convex app up and running locally we can highlight the first few lines of code that will help you use Convex in your own project. Follow along with the video or read on below.
Hooking up React components
Here's the first half of the code for the App
React component of the project
you have running locally. React components communicate with the Convex backend
via the hooks useQuery
and
useMutation
. This component rerenders
every time any user sends a new chat message.
import { useMutation, useQuery } from "../convex/_generated/react";
export default function App() {
const messages = useQuery("listMessages") || [];
const [newMessageText, setNewMessageText] = useState("");
const sendMessage = useMutation("sendMessage");
const [name] = useState(() => "User " + Math.floor(Math.random() * 10000));
async function handleSendMessage(event) {
event.preventDefault();
setNewMessageText("");
await sendMessage(newMessageText, name);
}
In the three highlighted lines above:
useQuery("listMessages")
returns either the result of running thelistMessages
query function on the server orundefined
if the query result is still loading. This hook causes the component to rerender whenever the query result changes.useMutation("sendMessage")
returns a function for telling the server to run thesendMessages
mutation.sendMessage(newMessageText, name)
is called from an event handler, sending its arguments to the server so thesendMessages
mutation function can be run with them.
Query functions
"listMessages"
above refers to a query function: a JavaScript (or
TypeScript) function that runs on Convex servers. The first
argument passed to useQuery
specifies the name of the file in the convex/
folder where this query function is the default export. This query function
returns every document in the messages table.
import { query } from "./_generated/server";
export default query(async ({ db }) => {
return await db.query("messages").collect();
});
Passing arguments to queries
As their first argument, query functions receive an object with a db
property
that can be used to read from and write to Convex tables. You can pass your own
arguments to query functions by adding them to the useQuery
call.
// src/App.jsx
useQuery("listMessages", 10, false)
│ │ └───────────────────┐
│ └─────────────┐ │
// convex/listMessages.js │ │
export default query(async ({ db }, maxMessages, repeat) => {
// gets the latest maxMessages, ordered by most recent instead of earliest
const messages = await db.query("messages").order("desc").take(maxMessages);
// sort the messages back into chronological order
messages.reverse();
if (repeat) {
return messages.map(m => {
return {...m, body: `${m.body} ${m.body}`};
};
}
return messages;
});
Breaking down the JavaScript syntax used here
const myFunction = async context => {
const db = context.db;
// Creates an array of all documents of the "messages" table
// similar to `SELECT * FROM messages`
const messages = await db.query("messages").collect();
return messages;
};
const myQuery = query(myFunction);
export default myQuery;
We can use
parameter destructuring
to pull out db
since that's the only property of the
query context object used in this function, and
combine
const myFunction = async ({ db }) => {
const messages = await db.query("messages").collect();
return messages;
};
const myQuery = query(myFunction);
export default myQuery;
Besides being one line shorter, calling mutation()
directly on the function we
wrote helps code editors infer the types of its parameters.
const myMutation = mutation(async ({ db }, body, author) => {
const message = { body, author };
await db.insert("messages", message);
});
export default myMutation;
Inlining the export default
is just a preference. We could also combine the
middle two lines.
There's something special about query functions: they run automatically! Every time data that would be read by a query function changes, the function runs again and notifies all subscribed clients of the change, causing the relevant React components to rerender with the new query result.
Query functions are for more than returning collections of documents: in addition to a querying the Convex database you can run any JavaScript you need.
How would you change this query function to...
Return chat messages without revealing their authors?
export default query(async ({ db }) => {
const allMessages = await db.query("messages").collect();
return allMessages.map(m => {
return {
...m,
author: "anonymous",
};
});
});
How would you write a query function in a new JavaScript file in the convex/
folder...
called getMessagesByAuthor.js
that returns only messages by a specific author?
Since query functions are just JavaScript, array methods like filter are often the simplest way to transform documents before returning them. This is a great way to get started.
export default query(async ({ db }, author) => {
const allMessages = await db.query("messages").collect();
return allMessages.filter(m => m.author === author);
});
You can use this new query in a React component with
const ownMessages = useQuery("getMessagesByAuthor", name);
But as tables get larger, it's more efficient to use the query builder to filter for the results we want instead processing an array containing the entire table of documents in JavaScript.
export default query(async ({ db }, author) => {
return await db
.table("messages")
.filter(q => q.eq(q.field("author"), author))
.collect();
});
Using an index would be more efficient still.
called getAuthors.js
that returns a list of authors and how many messages they have posted?
// convex/.js
export default query(async ({ db }, maxMessages) => {
const allMessages = await db.query("messages").collect();
const authors = {};
for (const message of allMessages) {
authors[message.author] = (authors[message.authors] || 0) + 1;
}
return authors;
});
If you're thinking "hmmm, it sounds like there should be two tables here, one for messages and one for authors, with a foreign key in messages to specify the author," you're not wrong. Convex works best as a relational database!
See querying the database to learn about filters, references to documents in other tables (AKA foreign keys), ordering results, and indexes.
Mutation functions
Mutation functions are also normal JavaScript or TypeScript
functions that run on Convex servers when requested. Clicking the submit button
in our app tells the Convex backend to run the sendMessage
mutation function
with the arguments provided.
import { mutation } from "./_generated/server";
export default mutation(async ({ db }, body, author) => {
const message = { body, author };
await db.insert("messages", message);
});
This mutation function creates an object with two properties, body
and
author
, and inserts that object into the messages table. Here's what
your dashboard might look like after sending a
few messages from two browser windows. You can see that documents in Convex
tables get two other properties for free:
_id
and _creationTime
.
Since mutations are just JavaScript, see if you can figure out how to make the
following changes to the listMessages mutation function. If npx convex dev
is
still running, changes you make to convex/listMessages.js
will be reflecting
as soon as you save the file.
How would you change this mutation function...
to make every submitted chat messages ALL CAPS?
body
is a string, so we can call .toUpperCase()
on it to get the shouty
version.
export default mutation(async ({ db }, body, author) => {
const message = { body: body.toUpperCase(), author };
await db.insert("messages", message);
});
to require messages to be longer than 10 characters?
Use an if statement! Remember, mutation functions are just JavaScript.
export default mutation(async ({ db }, body, author) => {
if (body.length < 10) {
return "Message is not long enough!";
}
await db.insert("messages", { body: body.toUpperCase(), author });
return "ok";
});
Mutation functions can also return values back to the UI, so let's modify the React component to do something with the result.
// src/App.jsx
const [errorMessage, setErrorMessage] = useState(null);
async function handleSendMessage(event) {
event.preventDefault();
setNewMessageText("");
const result = await sendMessage(newMessageText, name);
if (result === "ok") {
setError(undefined);
} else {
setError(result);
}
}
to prevent users from from spamming chat with too many messages? Hint: mutations can read data too!
We can check the creation time of the last message sent by a user and compare it with the current time. That might make you worry about race conditions: could two messages slip in together, both reading the same latest message? Don't worry, mutation functions run transactionally!
export default mutation(async ({ db }, body, author) => {
const now = Date.now();
const allMessages = await db.query("messages").collect();
const previousMessages = allMessages.filter(m => m.author === author);
const lastMessageSent = Math.max(previousMessages.map(m => m._creationTime));
if (lastMessageSent + 1000 * 10 > now) {
return "Too soon to send a new message";
}
await db.insert("messages", { body: body.toUpperCase(), author });
});
Being able to run arbitrary JavaScript in mutations and query functions is powerful! But it's often more efficient to express filters like this through the query builder, or even to use indexes (not shown here) to avoid bringing every document in a table into memory.
export default mutation(async ({ db }, body, author) => {
const now = Date.now();
const previousMessages = await db
.table("messages")
.filter(q => q.eq(q.field("author"), author))
.collect();
const lastMessageSent = Math.max(previousMessages.map(m => m._creationTime));
if (lastMessageSent + 1000 * 10 > now) {
return "Too soon to send a new message";
}
await db.insert("messages", { body: body.toUpperCase(), author });
});
This is just the beginning of a rate limit: the most glaring issue with it is that users could send in a different user name to evade it! To prevent this you'd want to add user authentication.
The Convex client
Back in the UI code, useQuery()
and
useMutation()
could only be used in the
App
component because a ConvexProvider
above it in the React component tree makes a
ConvexReactClient
instance available.
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { ConvexProvider, ConvexReactClient } from "convex/react";
const address = import.meta.env.VITE_CONVEX_URL;
const convex = new ConvexReactClient(address);
ReactDOM.render(
<StrictMode>
<ConvexProvider client={convex}>
<App />
</ConvexProvider>
</StrictMode>,
document.getElementById("root")
);
- The url for a Convex deployment is retrieved from an environment variable. This URL might point to a production deployment in prod and your own personal development deployment when developing locally.
- The
ConvexReactClient
constructor uses this URL to create a websocket connection to this server. - A
ConvexProvider
component uses a React context to provide all descendant React elements with this connection to the server.
What next?
This concludes a whirlwind tour of a Convex application: how to write Convex
functions and how to hook them up. If you haven't yet, do try modifying the
code: while npx convex dev
is running, modifying a query will update the
results in your browser instantly. If you have any trouble or want to ask
questions, we're here to help.
📄️ Writing Convex Functions
Functions describe server behavior.
📄️ Document IDs
Create complex, relational data models using IDs.
📄️ Querying the Database
Push queries down to the database.
📄️ Schemas
End-to-end type safety requires typing your tables.
📄️ Users and Auth
Add authentication to your Convex app.
📄️ Deploying your project
Share your Convex backend and web app with the world.