Skip to main content

Basic Concepts

We just created an application comprised of a local React frontend storing data in a remote Convex deployment.

Convex Client-Server

This application was defined completely by the files in our local directory but it's a real distributed application running in the cloud. The persistent shared state is stored in Convex and is available to any browser running your app that wants to interact with it. We can verify this by checking out the data explorer on your Convex deployment's dashboard.

Within your app's directory, open up the dashboard for your Convex deployment using the CLI:

npx convex dashboard

Then, take a look at the Data view on the dashboard.

Counter Data

There's a single table, counter_table, with a single document. The document has the counter field that we defined, plus two system fields created automatically:

  • _id: A unique identifier for this document.
  • _creationTime: The time this document was created.

How it's made

What's our code actually doing? Let's start by checking out the React app. There are two files to look at here: src/main.tsx, the setup code that hooks up the Convex client and the render that kicks off the React runtime; and src/App.tsx, where our application logic lives. Let's start with src/main.tsx.

src/main.tsx
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import convexConfig from "../convex.json";

const convex = new ConvexReactClient(convexConfig.origin);

ReactDOM.render(
<StrictMode>
<ConvexProvider client={convex}>
<App />
</ConvexProvider>
</StrictMode>,
document.getElementById("root")
);

Along with some boilerplate common to many React applications is code that that creates a ConvexReactClient instance with credentials found in the convex.json file that was created when we ran convex init.

import { ConvexProvider, ConvexReactClient } from "convex/react";
import convexConfig from "../convex.json";

const convex = new ConvexReactClient(convexConfig.origin);

We'll use this instance throughout our app. The ConvexProvider makes the client available to all descendants in the React component tree.

<ConvexProvider client={convex}>
<App />
</ConvexProvider>

Now let's move on to src/App.tsx. This component will be the focus for the rest of this guide as we explore the key concepts of Convex: query functions and mutation functions.

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

export default function App() {
// Watch the results of the Convex function `getCounter`.
const counter = useQuery("getCounter") ?? 0;

const increment = useMutation("incrementCounter");
function incrementCounter() {
// Execute the Convex function `incrementCounter` as a mutation
// that updates the counter value.
return increment(1);
}

return (
<main>
<div>Here's the counter:</div>
<div>{counter}</div>
<button onClick={incrementCounter}>Add One!</button>
</main>
);
}

Taking this apart, this first line imports two React hooks typed specifically for this application, so argument and return types will be type-checked.

import { useQuery, useMutation } from "../convex/_generated/react";

These hooks are coming from module called convex/_generated/react.ts so they'll by typed specifically for this application. They look up the React component tree to find the Convex client provided by a ConvexProvider.

Let's take a look at the JSX that creates our actual React component. This will look pretty familiar to React developers:

return (
<main>
<div>Here's the counter:</div>
<div>{counter}</div>
<button onClick={incrementCounter}>Add One!</button>
</main>
);

The value of the counter is displayed and incrementCounter is being called when the button is clicked to make the counter increase by one.

Query Functions

Let's dig in to how the state is managed in the counter value.

// Watch the results of the Convex function `getCounter`.
const counter = useQuery("getCounter") ?? 0;

Here's where Convex gets involved. It provides useQuery, a hook that begins watching a corresponding Convex query when a component is mounted. This watch fetches the value of the query function and triggers a rerender whenever necessary. Butwhen is this necessary? That is, how often is the code that renders the component run?

The simple answer is: any time the result of the query could change.

Convex detects any time the underlying data changes and automatically re-executes the query and rerenders the React component.

Let's take a look at the query function body itself now. You'll find it in the file convex/getCounter.ts:

convex/getCounter.ts
import { query } from "./_generated/server";

export default query(async ({ db }): Promise<number> => {
const counterDoc = await db.table("counter_table").first();
console.log("Got stuff");
if (counterDoc === null) {
return 0;
}
return counterDoc.counter;
});

Query functions are normal TypeScript/JavaScript functions that run within your Convex deployment. They are defined using the generated query wrapper. They will run with one extra argument than they are called with in frontend code: a QueryCtx with a db property that can be used to access Convex tables. db implements the DatabaseReader interface, which contains all the methods we need to read data from the database.

You can write (and even export, useful for unit tests) many functions in a single file in the convex/ directory, but only exported functions wrapped with query or mutation comprise your public API. Since this function is the default export of the module, it is called with useQuery("getCounter"), the filename of the module relative to the convex/ directory without the .ts extension.

tip

Whenever you add additional functions in the convex/ directory the generated file convex/_generated/react.ts file where the useQuery function specifically typed for your application is defined needs to be regenerated. This happens during npx convex push or can be run explicitly with npx convex codegen.

If you're not using TypeScript or need to skip this code generation step, you can always use useQueryGeneric() at the cost of losing autocompletion and, if you're using TypeScript, needing to provide type information manually.

This particular query uses the first() method to pull a single document out of a table called counter_table. You may recall seeing this document in the data browser in your Convex deployment dashboard earlier in the tutorial.

If the document exists, we return the counter field; otherwise, we return 0.

End to end, the dependency chain from storage to computation to ConvexReactClient to React component represents a comprehensive dataflow that seamlessly re-renders your app's components as needed.

Mutation Functions

What about that code that runs on the button click? This is updating state via a Convex mutation function. This mutation function is also a TypeScript file in the convex/ directory, called incrementCounter.ts.

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

export default mutation(async ({ db }, increment: number) => {
let counterDoc = await db.table("counter_table").first();
if (counterDoc === null) {
db.insert("counter_table", {
counter: increment,
});
// console.log messages appear in your browser's console and the Convex dashboard.
console.log("Created counter.");
} else {
counterDoc.counter += increment;
db.replace(counterDoc._id, counterDoc);
console.log(`Value of counter is now ${counterDoc.counter}.`);
}
});

Like query functions, mutation functions always run in your Convex deployment. Each time the button is clicked, the mutation reads the current document from the table, increments the counter field, and writes the altered document back.

The mutation wrapper also causes an extra ctx argument to be passed when the function runs, but this is a MutationCtx with a db property that can be used to modify the database, so its db property implements the DatabaseWriter interface.

As we discovered in the Queries section, any users currently on the site will have been subscribed to this change by virtue of the getCounter binding, and so will have their component rerendered with the new counter value automatically when any user changes it.

tip

If you pay close attention, you might detect a tiny lag between when you click the "Add One!" button and when the counter updates. This latency is the time it takes Convex to run your mutation and sync the changed queries back to the client.

To build an even snappier UI, you can add Optimistic Updates to your mutation calls.

Data Race Issues?

Readers experienced with async programming might be worried about the mutation function above; what happens if a read and write interleave when two people have the page open at the same time? Couldn't one of the increment operations be effectively lost?

You're all safe here. Convex runs these functions as transactions and provides strong consistency via Optimistic Concurrency Control, so it'll just retry the mutation on any data races until all changes can commit cleanly and atomically. Convex makes sure the mutation function is safe to replay until it succeeds.

If you want the gritty details, you can read more in the guide about functions and the specifics of how mutations work in Convex.

Function registration

Any time npx convex push is run, the newest version of any functions wrapped with query or mutation in the convex/ directory are uploaded into your deployment for immediate use. For each of the files in this directory, the filename is used as the function name, and the default export is expected to be a function that represents the body of the query or mutation.

In the case of our example with the counter, convex/getCounter.ts creates a query and sets it as the default export. App.tsx uses "getCounter" to refer to the function. These name strings autocomplete in the useQuery hook generated for your application.

useQuery("getCounter");

Logging and debugging

If any of your query or mutation functions encounter errors, and/or you want to emit console logs, those errors and logs are passed back down to the ConvexReactClient and echoed into the browser's console. Open up those developer tools and check out your browser's console to debug any issues with your Convex functions.