Mutating data with Supabase

In this guide, we'll learn how to mutate data asynchronously in Supabase using server actions and react query. Usually, the scenario looks like this.

  1. The server and client components for the route are loaded.
  2. User performs an action that triggers a mutation, eg. clicking a button, filling out a form, etc.
  3. The client sends a request to the server to perform the mutation.

In previous versions of Next.js, we would have to write API routes to handle the request. But as of Next.js 13, and now in 14 and beyond, we have a new way to handle this. These are called route handlers and server actions.

What are route handlers?

In Next.js 14, Route handlers have replaced API routes. These are simple route.ts files that can be placed in the app folder at any level.

Here is an example of a route handler that handles a POST request to /api/hello.

pages/api/hello.ts
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'POST') {
    // Process a POST request
    const { name, email } = req.body;
    res.status(200).json({ name, email });
  } else {
    // Handle any other HTTP method
    res.status(405).json({ message: 'Method not allowed' });
  }
}

As of Next.js 14, Route handlers have replaced API routes. These are simple route.ts files that can be placed in the app folder at any level. They can handle multiple HTTP methods (GET, POST, PUT, DELETE, etc.) in a single file.

Here's an example of a simple POST request using Route handlers:

app/api/hello/route.ts
import { NextResponse, NextRequest } from 'next/server';
 
export async function POST(request: NextRequest) {
  const formData = await request.formData();
  const name = formData.get('name');
  const email = formData.get('email');
  return NextResponse.json({ name, email });
}

Now both the above examples are functionally equivalent. But the latter is much more concise and easier to read.

What are server actions?

Server actions are convenient wrappers around route handlers that allow you to perform mutations on your data within your client / server components. Instead of writing a route handler, you can simply write a server action and call it from your client component.

Here's an example of a server action that handles a POST request similar to the example above.

app/server-actions/hello.ts
'use server';
import { NextResponse, NextRequest } from 'next/server';
 
export async function hello(formData: FormData) {
  const name = formData.get('name');
  const email = formData.get('email');
  return {
    name,
    email,
  };
}

And here's how you would call it from your client component.

app/some-route/page.tsx
'use client'
import { useMutation } from 'react-query';
function MyForm(){
  const { mutate } = useMutation(async (formData: FormData) => {
    return await hello(formData);
  }, {
    onSuccess: (data) => {
      const { name, email } = data;
      // Do something with the data
    }
  });
  return <form onSubmit={(e) => {
    e.preventDefault();
    mutate(new FormData(e.target as HTMLFormElement));
  }}>
    <input type="text" name="name" />
    <input type="email" name="email" />
    <button type="submit">Submit</button>
  </form>
}

In the above example, we are using the useMutation hook from react-query to call our server action.

  • Note how we are directly invoking the hello server action from our client component.
  • This will internally be converted to a POST request to a route handler automatically created by Next.js and mapped to a route.

useToastMutation hook

Nextbase starter kit provides a convenience hook called useToastMutation that can be used to display a toast notification throughout the lifecycle of a mutation. It is a thin wrapper around useMutation from react-query and uses sonner to display toast notifications.

app/some-route/page.tsx
'use client'
 
import { useToastMutation } from '@/hooks/useToastMutation';
function MyForm(){
  const { mutate } = useToastMutation(async (formData: FormData) => {
    return await hello(formData);
  }, {
    onSuccess: (data) => {
      const { name, email } = data;
      // Do something with the data
    },
    loadingMessage: 'Calling function...',
    successMessage: 'Successful!',
    errorMessage: 'Something went wrong!',
  });
  return <form onSubmit={(e) => {
    e.preventDefault();
    mutate(new FormData(e.target as HTMLFormElement));
  }}>
    <input type="text" name="name" />
    <input type="email" name="email" />
    <button type="submit">Submit</button>
  </form>