Guides & Tutorials

Global State Management on Netlify with Convex

Guides & Tutorials

Global State Management on Netlify with Convex

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
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.


The first thing we’re going to need is a free account with Convex. You can sign up for the beta at 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:, 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} />

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

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!


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 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> 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!

Keep reading

Recent posts

Streamline website development with Netlify's Composable Web Platform

Untangle development bottlenecks

Access the guide