Next.js Discord

Discord Forum

Proper data flow in Supabase Next app

Answered
Masai Lion posted this in #help-forum
Open in Discord
Masai LionOP
I'm trying to create a page in my Next app that was created from a Supabase boilerplate that allows a user to get a list of rows and then update one of those rows. I ran into a problem when I tried to create an update function tied to a button onClick where it gave me an error because the page is a server component

From there, I tried to split up code into server and client components, but ran into problems where I still can't run server code (Supabase server client which stores the user session) in the client component. At one point I thought I figured it out by passing a function with use server at the top as a prop to the client component, but when I clicked the button, it didn't do anything.

What am I missing? It's been a while since I've worked in React, but I still understand it's concepts. I'm new to Next, but if this is how it works, I'll probably abandon it pretty quickly as it shouldn't be this difficult for me to pull data from the server and then update it
Answered by Plague
You need to move the Server Action to a separate file and import it directly into the client component and call it that way.

See this example: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#event-handlers
View full answer

15 Replies

Masai LionOP
Here is what I have right now. It doesn't error at all, but the approveFile function also doesn't run at all. I get a console.log statement from the onClick function, but nothing from the inner logging of the approveFile function
// page.tsx

import { createClient } from '@/utils/supabase/server';
import { File, FileTable } from './file';

async function approveFile(file: File) {
  'use server';
  const supabase = createClient();
  console.log("Approve file", file);
  await supabase.functions.invoke("approve-file", {
    body: JSON.stringify(file),
  })
}

export default async function PredictedReceipts() {
  const supabase = createClient();
  const { data: files, error } = await supabase.from('files').select('*').returns<File[]>();

  if (error) {
    console.error(error);
    return <div>Error: {error.message}</div>;
  }

  return (
    <>
      <h1 className="text-2xl font-medium text-center">Title</h1>

      <FileTable files={files} approveAction={approveFile} />
    </>
  );
}
// file.tsx
'use client';
import { Button } from '@/components/ui/button';
import { Image } from 'lucide-react';

export interface File {
  id: string;
  name: string;
  mime_type: string;
}

export function FileTable(props: Readonly<{
  files: File[];
  approveAction: (file: File) => Promise<void>;
}>) {
  return (
    <table className="table-auto table border-collapse">
      <thead className="table-header-group">
        <tr className="table-row">
          <th className="table-cell text-center border-b px-3">Link</th>
          <th className="table-cell text-center border-b px-3">Name</th>
          <th className="table-cell text-center border-b px-3"></th>
        </tr>
      </thead>
      <tbody className="table-row-group">
        {props.files?.map((file) => (
          <tr key={file.google_drive_id} className="table-row">
            <td className="table-cell text-center border-b px-3">
              <a href={`https://drive.google.com/file/d/${file.id}/view`} target="_blank">
                <Image />
              </a>
            </td>
            <td className="table-cell text-center border-b px-3">{file.name}</td>
            <td className="table-cell text-center border-b px-3 py-1">
              <Button
                size={'sm'}
                onClick={async() => {
                  console.log("ReceiptsTable onClick handler");
                  await props.approveAction(file);
                }}
              >
                Approve
              </Button>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
You need to move the Server Action to a separate file and import it directly into the client component and call it that way.

See this example: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#event-handlers
Answer
Masai LionOP
I moved it to an action and imported it like in the docs you linked. Now I get an error:

Error: 
  × You're importing a component that needs next/headers. That only works in a Server Component which is not supported in the pages/ directory
Masai LionOP
// file.tsx (using imported action, truncated)
'use client';
import { moveReceipt } from '@/app/actions/receipt';
...

export function ReceiptsTable(props: Readonly<{
  files: File[];
}>) {
  return (
...
              <Button
                size={'sm'}
                onClick={async() => {
                  console.log("ReceiptsTable onClick handler");
                  await moveReceipt(file);
                }}
              >
                Approve
              </Button>
...
}
// actions/receipt.ts
import { createClient } from "@/utils/supabase/server";
import { File } from "../user/receipts/receipts";

export async function moveReceipt(file: File) {
  const supabase = createClient();

  console.log("Move receipt", file);
  // await supabase.functions.invoke("move-receipt", {
  //   body: JSON.stringify(fileId),
  // });
}
did you write "use server" at the top of the actions file?
it needs that
Other than that, this looks fine. If you add "use server" it should work as expected.
Masai LionOP
Yeah, you were right that I needed to add that. Now I don't get an error. But the imported function doesn't do anything. I expect to see at least the console.log statement, but I only see the one in the client page
Masai LionOP
Holy crap!.. Wow!!.. and there is the missing piece that I needed to know. THANK YOU!!
No problem