Anshuman Bhardwaj Anshuman, a.k.a Superman–is a seasoned software engineer who loves writing and teaching. A craftsman of React and JavaScript, he enjoys solving everyday problems with code and sharing his learnings with the world.

A guide to using Convex for state management

9 min read 2646

A Guide to Using Convex for State Management

Modern-day frontend frameworks like React have made state management easier for developers, but using solutions like Convex for state management are making it even easier. In React, the state of a component dictates how the UI will look, and while managing an application’s state has become relatively simple, the global state concept is still a pain for developers.

In this tutorial, we will build a full-stack Next.js application with Convex for global state management. We’ll also implement Convex functions to query and update the data. By the end of this tutorial, we will have deployed the final application to Vercel — feel free to follow along with this walkthrough using this GitHub repository.

What is Convex?

Consider, for example, applications keeping track of financial transactions that work with a global state all clients can change and observe in real-time. Such applications are hard to develop because developers need to sync the state between applications and take care of ACID properties.

Convex aims to solve this problem by providing a full-stack solution including data storage, retrieval, and mutations, all built into an SDK for global state management. Its serverless approach is efficient and makes for a highly scalable platform. Convex is a developer-first platform, with a reactive architecture that aligns well with React, and the SDK additionally has support for features like optimistic updates and subscriptions.

Prerequisites

You’ll need Node.js, npm, or yarn, and a code editor like VS Code installed on your computer. You’ll also need a GitHub account to use with Convex.

Set up a Next.js project

The application will allow users to see a list of existing posts and submit new blog posts. A blog post will contain a title, body, and the author’s name.

To get started, run the following command to set up a new Next.js project:

npx [email protected] convex-example --typescript

Open the project inside the code editor and update the pages/index.tsx file to display a form in order to create a blog post:

// pages/index.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import styles from '../styles/Home.module.css'
import { useCallback, useState } from 'react'

const Home: NextPage = () => {
  const [author, setAuthor] = useState('')
  const [title, setTitle] = useState('')
  const [body, setBody] = useState('')

  const createPost = async () => {
    // TODO: create a new post inside database
    console.log({ author, title, body  })
    // reset the inputs after submission
    setAuthor('')
    setBody('')
    setTitle('')
  }

  return (
      <div className={styles.container}>
        <Head>
          <title>Next.js with Convex</title>
          <meta name="description" content="Generated by create next app" />
          <link rel="icon" href="/favicon.ico" />
        </Head>

        <main className={styles.main}>
          <h1 className={styles.title}>
            Welcome to <a href="https://nextjs.org">Next.js</a> with{' '}
            <a href="https://convex.dev">Convex</a>
          </h1>
          <input
              type={'text'}
              value={title}
              placeholder={'Title'}
              className={styles.inputStyles}
              onChange={(event) => setTitle(event.target.value)}
          />      
          <input
              type={'text'}
              value={author}
              placeholder={'Author'}
              className={styles.inputStyles}
              onChange={(event) => setAuthor(event.target.value)}
          />
          <textarea
              value={body}
              rows={5}
              placeholder={'Post body '}
              className={styles.inputStyles}
              onChange={(event) => setBody(event.target.value)}
          />
          <button className={styles.button} onClick={createPost}>
            Create post
          </button>
        </main>
      </div>
  )
}

export default Home

Update the styles/Home.module.css to the following:

/* styles/Home.module.css */
.container {
  padding: 0 2rem;
  display: flex;
  flex-direction: column;
}

.main {
  padding: 4rem 0;
  flex: 10;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.button {
  font-size: 1rem;
  font-weight: 800;
  cursor: pointer;
  margin: 0.5rem;
  padding: 0.5rem;
  text-decoration: none;
  border: 1px solid #eaeaea;
  border-radius: 10px;
  transition: color 0.15s ease, border-color 0.15s ease;
  width: 200px;
}

.button:hover,
.button:focus,
.button:active {
  color: #0070f3;
  border-color: #0070f3;
}

.inputStyles {
  width: 300px;
  margin: 10px auto;
}

Run npm run dev to start the application, then open http://localhost:3000/ in a web browser.

Convex Blog Post Form

The form for creating new posts is ready. Now, you have to implement the logic to save and read the data using Convex.

Set up Convex

Convex provides a JavaScript SDK that you can use in your project.

  1. Run npm i convex to install the Convex package
  2. Inside the project, run npx convex login, which will open a page in the browser to log into Convex using your GitHub account
  3. After logging in, run npx convex init to initialize the Convex project with a convex.json and .env.local for configuration. This command will also create a convex/ directory to write functions into

(Note: This command will prompt you for a project name)

Finally, update the pages/_app.tsx to add ConvexProvider to the complete application. The ConvexProvider will allow you to use React hooks provided by Convex across the application.

// pages/_app.tsx
import '../styles/globals.css'
import type { AppProps } from 'next/app'

import { ConvexProvider, ConvexReactClient } from 'convex/react'
import convexConfig from '../convex.json'
const convex = new ConvexReactClient(convexConfig.origin)

function MyApp({ Component, pageProps }: AppProps) {
  return (
      <ConvexProvider client={convex}>
        <Component {...pageProps} />
      </ConvexProvider>
  )
}

export default MyApp

Add state management using Convex

With Convex set up in the project, it’s time to create a data model and connect the frontend with the database.

Define the schema

Inside the convex/ folder, create a new file, schema.ts, to define the schema for blog posts. The defineTable function will create a posts table inside Convex.

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

export default defineSchema({
  posts: defineTable({
    title: s.string(),
    author: s.string(),
    body: s.string(),
  }),
})

Now, run npx convex codegen to generate type definitions for the posts schema to improve code completions. This will allow you to reference posts as Document<'posts'>.



Implement Convex functions

Convex functions allow the frontend to communicate with the database in two ways: queries and mutation.

These functions are exported from files within the convex directory and they are deployed as serverless functions to execute database interactions.

The frontend needs to read the available posts. For that, create a new file; convex/getPosts.ts. This file exports a query function that returns all available posts from the database.

// convex/getPosts.ts
import { query } from './_generated/server'
import { Document } from './_generated/dataModel'

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

Inside the convex/ folder, create a new file called addPost.ts. This file exports a mutation function to allow users to add a new post to the database. The function accepts a post object as an argument.

// convex/addPost.ts
import { mutation } from './_generated/server'

export default mutation(
  async (
    { db },
    post: { author: string; body: string; title: string }
  ) => {
    await db.insert('posts', post)
  }
)

Run npx convex push to generate the type definitions and deploy the functions to Convex.

Connect the component with Convex functions

Convex provides useQuery and useMutation hooks to interact with the database using the functions implemented above.

Add the useMutation hook to the Home component and update the createPost function to call the addPost mutation function with the post data.

// pages/index.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import styles from '../styles/Home.module.css'
import { useCallback, useState } from 'react'
import {useMutation} from "../convex/_generated/react";

const Home: NextPage = () => {
  const addPost = useMutation('addPost')

  const [author, setAuthor] = useState('')
  const [title, setTitle] = useState('')
  const [body, setBody] = useState('')

  const createPost = async () => {
    await addPost({ body, author, title});
    // reset the inputs after submission
    setAuthor('')
    setBody('')
    setTitle('')
  }

  return (
      // return the component
  )
}

export default Home

Add the useQuery hook to fetch and display the list of posts from the database. The useQuery hook will return undefined while loading data, and a list of posts afterward.

// pages/index.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import styles from '../styles/Home.module.css'
import { useCallback, useState } from 'react'
import {useMutation, useQuery} from "../convex/_generated/react";

const Home: NextPage = () => {
  const posts = useQuery('getPosts')
  const addPost = useMutation('addPost')

  const [author, setAuthor] = useState('')
  const [title, setTitle] = useState('')
  const [body, setBody] = useState('')

  const createPost = async () => {
    await addPost({ body, author, title});
    // reset the inputs after submission
    setAuthor('')
    setBody('')
    setTitle('')
  }

  return (
      <div className={styles.container}>
        <Head>
          <title>Next.js with Convex</title>
          <meta name="description" content="Generated by create next app" />
          <link rel="icon" href="/favicon.ico" />
        </Head>

        <main className={styles.main}>
          <h1 className={styles.title}>
            Welcome to <a href="https://nextjs.org">Next.js</a> with{' '}
            <a href="https://convex.dev">Convex</a>
          </h1>
          {posts ? (
              <>
                <p className={styles.description}>
                  {'Total posts:'} {posts.length}
                </p>

                <ul>
                  {posts.map((post) => (
                      <li key={post._id.toString()}>{post.title}</li>
                  ))}
                </ul>
              </>
          ) : (
              'Loading posts...'
          )}
          <input
              type={'text'}
              value={title}
              placeholder={'Title'}
              className={styles.inputStyles}
              onChange={(event) => setTitle(event.target.value)}
          />      
          <input
              type={'text'}
              value={author}
              placeholder={'Author'}
              className={styles.inputStyles}
              onChange={(event) => setAuthor(event.target.value)}
          />
          <textarea
              value={body}
              rows={5}
              placeholder={'Post body '}
              className={styles.inputStyles}
              onChange={(event) => setBody(event.target.value)}
          />
          <button className={styles.button} onClick={createPost}>
            Create post
          </button>
        </main>
      </div>
  )
}

export default Home

Your application is ready now! Open http://localhost:3000 to see it in action:

Convex Next.js Create App

You’ll notice that the list of posts gets updated automatically whenever you create a new post.

This behavior is possible due to the end-to-end reactivity of the Convex global state; every component using the query gets updated whenever the data changes.

Managing Convex

Run npx convex dashboard to log in to the Convex dashboard to manage your application data, view logs, and see the metrics for function execution and read/writes.

Convex Dashboard

Secure the application

Keeping the application data secure is crucial, and Convex simplifies protecting your data using identity providers. Convex comes with first-class support for Auth0 out of the box, and you can set it up in no time.

Create an Auth0 application

Log into your Auth0 dashboard and create a new Single Page Web Application. You can sign up for a free account on Auth0 if you don’t have an account already.

Create Auth0 Application

Copy the Domain and Client ID from the settings page of this new application and save them for later.

Copy Client ID Domain

In the application settings page, add http://localhost:3000 into the “Allowed Callback URLs” field as shown below. This will enable http://localhost:3000 to use Auth0 for login during development.

Add Local Host ID

Set up Auth0

Start by running npm i @auth0/auth0-react to install Auth0 in your project.

Then, run npx convex auth add to add Auth0 as the identity provider to Convex. This command will prompt you for the Domain and Client ID copied earlier.

Create a new folder called components/ at the root of the project and add a new file called Login.tsx for the Login component that has a button to prompt users for login.

// components/Login.tsx
import { useAuth0 } from '@auth0/auth0-react'

export function Login() {
  const { isLoading, loginWithRedirect } = useAuth0()
  if (isLoading) {
    return <button className="btn btn-primary">Loading...</button>
  }
  return (
    <main className="py-4">
      <h1 className="text-center">Convex Chat</h1>
      <div className="text-center">
        <span>
          <button className="btn btn-primary" onClick={loginWithRedirect}>
            Log in
          </button>
        </span>
      </div>
    </main>
  )
}

Update the pages/_app.tsx to replace the ConvexProvider with ConvexProviderWithAuth0.

import '../styles/globals.css'
import { ConvexProviderWithAuth0 } from 'convex/react-auth0'
import { ConvexReactClient } from 'convex/react'
import convexConfig from '../convex.json'
import { AppProps } from 'next/app'
import { Login } from '../components/Login'

const convex = new ConvexReactClient(convexConfig.origin)
const authInfo = convexConfig.authInfo[0]

function MyApp({ Component, pageProps }: AppProps) {
    return (
        <ConvexProviderWithAuth0
            client={convex}
            authInfo={authInfo}
            loggedOut={<Login />}
        >
            <Component {...pageProps} />
        </ConvexProviderWithAuth0>
    )
}

export default MyApp

Now, when you open the application http://localhost:3000, you’ll see a login button instead of the post form.

Login With Auth0

Integrate Auth0 with Convex

Now that you have Auth0 configured, you can secure the mutation function. The mutation function provides the authentication information as the auth object. The addPost mutation will now reject any unauthenticated requests.

// convex/addPost.ts
import { mutation } from './_generated/server'

export default mutation(
    async (
        { db, auth },
        post: { author: string; body: string; title: string }
    ) => {
      const identity = await auth.getUserIdentity()
      if (!identity) {
        throw new Error('Called addPosts without authentication present!')
      }
      await db.insert('posts', post)
    }
)

You can also update the code on the frontend to use the logged-in user’s name as the author field:

// pages/index.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import styles from '../styles/Home.module.css'
import { useCallback, useState } from 'react'
import {useMutation, useQuery} from "../convex/_generated/react";
import {useAuth0} from "@auth0/auth0-react";

const Home: NextPage = () => {
  const {user} = useAuth0()
  const posts = useQuery('getPosts')
  const addPost = useMutation('addPost')

  const [title, setTitle] = useState('')
  const [body, setBody] = useState('')

  const createPost = async () => {
    if(user?.name) {
      await addPost({ body, author: user.name, title});
    }
    // reset the inputs after submission
    setBody('')
    setTitle('')
  }

  return (
      <div className={styles.container}>
        <Head>
          <title>Next.js with Convex</title>
          <meta name="description" content="Generated by create next app" />
          <link rel="icon" href="/favicon.ico" />
        </Head>

        <main className={styles.main}>
          <h1 className={styles.title}>
            Welcome to <a href="https://nextjs.org">Next.js</a> with{' '}
            <a href="https://convex.dev">Convex</a>
          </h1>
          {posts ? (
              <>
                <p className={styles.description}>
                  {'Total posts:'} {posts.length}
                </p>

                <ul>
                  {posts.map((post) => (
                      <li key={post._id.toString()}>{post.title}</li>
                  ))}
                </ul>
              </>
          ) : (
              'Loading posts...'
          )}
          <input
              type={'text'}
              value={title}
              placeholder={'Title'}
              className={styles.inputStyles}
              onChange={(event) => setTitle(event.target.value)}
          />
          <textarea
              value={body}
              rows={5}
              placeholder={'Post body '}
              className={styles.inputStyles}
              onChange={(event) => setBody(event.target.value)}
          />
          <button className={styles.button} onClick={createPost}>
            Create post
          </button>
        </main>
      </div>
  )
}

export default Home

Deploy to Vercel

To deploy the application, push your code (including convex.json) to a repository on GitHub and link it to your Vercel account:

Deploy To Vercel

Replace the build command with npx convex push && next build to push the latest functions to Convex while deploying, and add the CONVEX_ADMIN_KEY environment variable from the .env.local:

Update Deploy Settings

Once the application is deployed, copy the deployment URL (.vercel.app):

Copy Vercel Deployment

Add the URL to the Allowed Callback URLs list alongside the http://localhost:3000 in the Auth0 application settings.

Convex Dashboard

Conclusion

Your application is now deployed on Vercel. In this tutorial, we learned about global state management and how to deploy a Next.js application with state management using Convex.

We also learned about securing the application with Auth0 and deploying it to Vercel. You can extend the above application to use advanced features like optimistic updates and indexes to make it even faster.

You can try Convex for free today and read more about using it in their documentation.

LogRocket: Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — .

Anshuman Bhardwaj Anshuman, a.k.a Superman–is a seasoned software engineer who loves writing and teaching. A craftsman of React and JavaScript, he enjoys solving everyday problems with code and sharing his learnings with the world.

Leave a Reply