One of the biggest challenges when building a dynamic application is the management of global state: deploying a backend, interacting with a database, caching, and manually synchronizing data between client and server. Manual state management is not only complex, but any errors can easily result in an application displaying data that is stale, incorrect, or incomplete.
Convex is a global state management platform designed for the needs of dynamic application developers, obviating the need for complex error-prone application logic. Today we’re going to use Convex to transform a local React app into a fully-distributed dynamic application, deployed globally on Netlify, all in under 10 minutes.
Our local app
We’re going to start with a simplified Reddit clone with posts and updates.
Clone and run the sample code for a basic local-only version of this app:
git clone https://github.com/get-convex/creddit.git
cd creddit
npm install
npm run dev
Then load localhost:3000 in your browser to try adding and upvoting posts. It’s a bit of a stretch to call it a Reddit clone but we’re keeping things simple for now - additional features won’t fundamentally change the underlying implementation.
Now’s a good time to familiarize yourself with what this code is doing. The interesting code is in pages/index.tsx
. If you’re a React or Next.js developer this should all look pretty familiar. The posts are stored in the posts
state array and there are two functions that interact with it: handleAddPost
which adds a new post and handleUpvote
which increments the vote count for a post.
So far so good but the state in our app is all local. Refresh the page and the posts disappear. Open the page in a different browser tab and none of the posts will be there. Clearly this isn’t what we want.
Making a local React app global
The problem with our current app is that the posts
array is local state, whereas we want it to be global state. This would normally be a big change. Get your stopwatch ready because we’re going to fix this in ten minutes.
Convex Basics
Convex is a platform for global state management. It provides a backend database to store your state, allows you to query this state via server functions written in JavaScript or Typescript, and includes client libraries that synchronize state between client and server. Convex also provides the ability to subscribe to the output of a server function and to automatically rerender browser components when the output of that function changes.
We’re going to jump straight in to using Convex but if you get lost along the way the Convex docs are your friend.
Pre-reqs
The first thing we’re going to need is a free account with Convex. You can sign up for the beta at convex.dev. You’ll also need a free Netlify account which should take less than a minute to set up. Netlify is a cloud development platform that allows us to deploy our frontend code globally, straight from GitHub. Finally you’ll want to fork the creddit
git repository so you can make local changes and push them to your own repository.
Once you’ve got the pre-requisites under control it’s time to start your stopwatch. Let’s do this.
Install and initialize Convex
From the root of the creddit
directory, add the Convex development libraries and Convex command line tool:
npm i convex-dev
Then, create a new global Convex instance:
npx convex init --beta-key <your beta key>
This tool will have added a convex.json
file that includes your Convex config, a .env.local
file with your access token, and a convex/
directory to store your Convex server functions. Typically you’ll want to check convex.json
into git but keep your .env.local
file private.
Global data model
We really just want our posts
array to be global state stored in the cloud and to have the relevant changes synced locally. In Convex this is a pretty straightforward change.
Since we’ll be storing posts in Convex we’ll first change the Post
type definition in lib/common.ts
to use the Convex-assigned document ID as an identifier instead of the UUID we were using:
import { Id } from "convex-dev/values";
export type Post = {
_id: Id; // convex-assigned id
title: string;
date: number; // unix ms
votes: number;
};
Convex server functions
We also need some functions to interact with the global posts
state. The first function we need is to create a new post. Add the following to convex/addPost.ts
:
import { mutation } from "convex-dev/server";
// Add a post.
export default mutation(({ db }, title: string) => {
console.log("posting", title);
db.insert("posts", { title, date: Date.now(), votes: 1 });
});
This is a simple mutation function that inserts a new post into the posts
table, with the given title
, current date
and a vote count of 1
.
We also need to be able to upvote a post, so let’s add a function for that, in convex/upvote.ts
:
import { mutation } from "convex-dev/server";
import { Id } from "convex-dev/values";
import { Post } from "../lib/common";
// Upvote a post.
export default mutation(async ({ db }, id: Id) => {
console.log("upvoting", id);
const post: Post | null = await db.get(id);
if (post === null) {
throw new Error(`No post with id ${id}`);
}
db.update(id, { votes: post.votes + 1 });
});
This function takes a post id, fetches the existing post from the database, then writes an updated version with an incremented vote count.
Finally we need a way of querying all the current posts, which we add to convex/listPosts.ts
:
import { query } from "convex-dev/server";
import { Post } from "../lib/common";
// List all posts in sorted order.
export default query(async ({ db }): Promise<Post[]> => {
console.log("listing posts");
const posts: Post[] = await db.table("posts").collect();
posts.sort((a, b) => b.votes - a.votes);
return posts;
});
This is a simple query that performs a table scan followed by a sort of the posts in descending vote order.
There’s a small amount of boilerplate and syntax required in these functions but note what is not required: the listPosts
query just fetches the latest version of the posts - it doesn’t do any polling, doesn’t have any refresh logic, doesn’t deal with caching, etc. All of these are handled by some Convex magic we’ll get to shortly.
Now we have all the required query functionality you can run
npx convex codegen
to generate TypeScript stubs in convex/_generated.ts
that will help when building the rest of our code.
Connect the React App to Convex
We need to add a few lines of code to pages/_app.tsx
to link our code with the Convex libraries. First add the following to the import block:
import { ConvexProvider, ConvexReactClient } from "convex-dev/react";
import convexConfig from "../convex.json";
const convex = new ConvexReactClient(convexConfig.origin);
this imports the Convex libraries, reads the configuration file from convex.json
, and initializes a connection to Convex via ConvexReactClient
.
Next we want to wrap our top-level React component (<Component {...pageProps} />
) in a ConvexProvider
which will provide access to the convex
client connection to all descendants in the component tree:
<ConvexProvider client={convex}>
<Component {...pageProps} />
</ConvexProvider>
Calling Convex functions
Our last step is to call the addPost
and upvote
functions to mutate posts
state and to read from the listPosts
function to render the latest posts.
The useMutation
function provides a handle to call a Convex function on the server, for example:
const addPost = useMutation("addPost");
...
await addPost(newPostTitle);
Reading state from a Convex function is surprisingly streamlined:
const posts = useQuery("listPosts") ?? [];
The useQuery
hook binds the output of listPosts
to a local array posts
and updates it automatically whenever server data changes that affects the output of listPosts
. In practice what this means is that any time a new post shows up from any client, our local React component will re-render with the new state.
The code in pages/index.tsx
actually gets shorter as a result of switching to Convex.
Testing it out
Our work porting to Convex is done. Hopefully your code looks pretty similar to the finished product we have in the convex
branch of https://github.com/get-convex/creddit.git
.
Push your server functions to Convex using:
npx convex push
and then run your app locally via:
npm run dev
The app should look identical to before except that the state you’re seeing is actually global, stored in the Convex cloud. If you open a second browser tab and start adding/upvoting posts you’ll see them dynamically update in both tabs!
Deployment
Our application state is global but it’s hard to tell because the frontend is just running on our local host. Time to fix that by deploying the frontend to Netlify.
Commit your code changes and push them to your code repository on GitHub, GitLab or BitBucket. Don’t forget to check in the convex/
directory and the convex.json
file.
Now go to https://app.netlify.com/start and link your repository with Netlify. The default options should work fine:
Now click the Deploy Site button and we’re all set. Netlify will build your app, deploy it to a global CDN, and then give you a https://<project-name>.netlify.app
URL where it can be accessed.
Time to hit stop on your stopwatch. How did we do?
Next Steps
That was a bit of a whirlwind introduction but it covered the basics of deploying a global dynamic app with Convex and Netlify.
There’s lots of functionality you might want to add to your app:
- User accounts
- Links and comments on posts
- Better voting controls
- Optimistic updates for faster reactivity
- Image macros
Convex already supports these with authorization, foreign keys, complex business logic in server functions, optimistic updates, and Netlify external functions, but there are plenty more features on the way.
Take a look at the Convex documentation to learn more about the platform and start building your next dynamic global application!