Kembali ke Daftar Catatan

Server Action

#nextjs#server-actions#forms

Apa itu Server Action?

Server Action adalah fitur di Next.js 13+ yang memungkinkan kita menjalankan fungsi di server langsung dari komponen React di client, terutama digunakan untuk menangani pengiriman form tanpa membuat API route terpisah.


Contoh Penggunaan

Berikut adalah contoh sederhana penggunaan Server Action untuk membuat post:

export default function CreatePost() {
  async function create(formData: FormData) {
    'use server';

    const title = formData.get('title') as string;
    const slug = formData.get('slug') as string;
    const content = formData.get('content') as string;

    console.log(title, slug, content);

    await fetch('http://localhost:3001/post', {
      method: 'POST',
      body: JSON.stringify({ title, slug, content }),
      headers: {
        'Content-Type': 'application/json',
      },
    });
  }

  return (
    <section className="border border-white p-8">
      <h1 className="text-2xl">Create Post</h1>

      <form action={create}>
        <fieldset>
          <label htmlFor="title" className="block">
            Title
          </label>
          <input
            type="text"
            id="title"
            name="title"
            className="text-black bg-white"
            required
          />
        </fieldset>

        <fieldset>
          <label htmlFor="slug" className="block">
            Slug
          </label>
          <input
            type="text"
            id="slug"
            name="slug"
            className="text-black bg-white"
            required
          />
        </fieldset>

        <fieldset>
          <label htmlFor="content" className="block">
            Content
          </label>
          <textarea
            id="content"
            name="content"
            className="text-black bg-white"
            required
          ></textarea>
        </fieldset>

        <button type="submit" className="bg-blue-500 p-2">
          Create Post
        </button>
      </form>
    </section>
  );
}

Penjelasan Penting

formData.get('key') as string

Fungsi formData.get() mengembalikan tipe data berikut:

Karena kita tahu bahwa 'title', 'slug', dan 'content' berasal dari input teks dan wajib diisi (required), maka aman untuk menggunakan as string.

Namun, untuk keamanan runtime, lebih baik lakukan pengecekan:

const title = formData.get('title');
if (typeof title !== 'string') {
  throw new Error('Invalid title');
}

Komentar 'use server'

Baris 'use server' harus ditambahkan di awal fungsi agar Next.js tahu bahwa fungsi ini harus dijalankan di server. Fungsi ini tidak akan dijalankan di browser.


Cache Handling

Secara default, Next.js melakukan caching pada halaman. Jika kamu ingin memastikan halaman tidak di-cache, tambahkan baris berikut di bagian atas file page.tsx:

export const dynamic = 'force-dynamic';

Contoh:

export const dynamic = 'force-dynamic';

export default function CreatePost() {
  // ...
}

Ini berguna ketika kamu ingin selalu mendapatkan data terbaru setelah submit form.


Kelebihan Server Actions


Hal yang Perlu Diperhatikan


Tips Tambahan: Validasi Form dengan Zod

Untuk validasi form yang lebih kuat, gunakan library seperti Zod:

import { z } from 'zod';

const schema = z.object({
  title: z.string().min(1),
  slug: z.string().min(1),
  content: z.string().min(10),
});

async function create(formData: FormData) {
  'use server';

  const data = Object.fromEntries(formData.entries());
  const parsed = schema.parse(data); // akan error jika invalid

  await fetch('...', {
    method: 'POST',
    body: JSON.stringify(parsed),
    headers: {
      'Content-Type': 'application/json',
    },
  });
}

Memisahkan Server Action ke File Terpisah

Kita dapat memisahkan server action ke file terpisah agar lebih rapi dan modular. Pastikan 'use server'; ada di baris paling atas file tersebut.

File: app/action.ts

'use server';

export async function create(formData: FormData) {
  const title = formData.get('title') as string;
  const slug = formData.get('slug') as string;
  const content = formData.get('content') as string;
  console.log(title, slug, content);

  await fetch('http://localhost:3001/post', {
    method: 'POST',
    body: JSON.stringify({ title, slug, content }),
    headers: {
      'Content-Type': 'application/json',
    },
  });
}

File: app/create-post/page.tsx

import { create } from '@/app/action';

export default function CreatePost() {
  return (
    <section className="border border-white p-8">
      <h1 className="text-2xl">Create Post</h1>

      <form action={create}>
        <fieldset>
          <label htmlFor="title" className="block">
            Title
          </label>
          <input
            type="text"
            id="title"
            name="title"
            className="text-black bg-white"
            required
          />
        </fieldset>

        <fieldset>
          <label htmlFor="slug" className="block">
            Slug
          </label>
          <input
            type="text"
            id="slug"
            name="slug"
            className="text-black bg-white"
            required
          />
        </fieldset>

        <fieldset>
          <label htmlFor="content" className="block">
            Content
          </label>
          <textarea
            id="content"
            name="content"
            className="text-black bg-white"
            required
          ></textarea>
        </fieldset>

        <button type="submit" className="bg-blue-500 p-2">
          Create Post
        </button>
      </form>
    </section>
  );
}

Dengan cara ini, kode menjadi lebih modular, mudah diatur, dan siap untuk reuse di tempat lain.

Menampilkan loading

kita dapat menampilkan loading ketika submit form menggunakan useFormStatus contohnya

'use client';

import { create } from '@/app/action';
import { useFormStatus } from 'react-dom';

export function CreatePostForm() {
  return (
    <form action={create}>
      <fieldset>
        <label htmlFor="title" className="block">
          Title
        </label>
        <input
          type="text"
          id="title"
          name="title"
          className="text-black bg-white"
        />
      </fieldset>

      <fieldset>
        <label htmlFor="slug" className="block">
          Slug
        </label>
        <input
          type="text"
          id="slug"
          name="slug"
          className="text-black bg-white"
          required
        />
      </fieldset>

      <fieldset>
        <label htmlFor="content" className="block">
          Content
        </label>
        <textarea
          id="content"
          name="content"
          className="text-black bg-white"
          required
        ></textarea>
      </fieldset>
      <SubmitButton />
    </form>
  );
}

function SubmitButton() {
  const { pending } = useFormStatus();

  if (pending) {
    return <p>Creating ...</p>;
  }

  return (
    <button type="submit" className="bg-blue-500 p-2">
      Create Post
    </button>
  );
}

catatan hal ini bisa dilakukan hanya ketika didalam Form

Menampilkan Nilai dari Server Action

Kita bisa menampilkan pesan respon dari Server Action dan juga loading state menggunakan hook yang sama, yaitu useActionState. Contohnya seperti berikut:

Client Component (CreatePostForm.tsx)

'use client';

import { create } from '@/app/action';
import { useActionState } from 'react';

const initialState = {
  message: '',
};

export function CreatePostForm() {
  const [state, formAction, isPending] = useActionState(create, initialState);

  return (
    <form action={formAction}>
      <fieldset>
        <label htmlFor="title" className="block">
          Title
        </label>
        <input
          type="text"
          id="title"
          name="title"
          className="text-black bg-white"
        />
      </fieldset>

      <fieldset>
        <label htmlFor="slug" className="block">
          Slug
        </label>
        <input
          type="text"
          id="slug"
          name="slug"
          className="text-black bg-white"
          required
        />
      </fieldset>

      <fieldset>
        <label htmlFor="content" className="block">
          Content
        </label>
        <textarea
          id="content"
          name="content"
          className="text-black bg-white"
          required
        ></textarea>
      </fieldset>

      <SubmitButton pending={isPending} />

      {state?.message && <p>{state.message}</p>}
    </form>
  );
}

function SubmitButton({ pending }: { pending: boolean }) {
  if (pending) {
    return <p>Creating...</p>;
  }

  return (
    <button type="submit" className="bg-blue-500 p-2">
      Create Post
    </button>
  );
}

Server Action (action.ts)

'use server';

export type CreatePostResponse = {
  message: string;
};

export async function create(
  prevData: CreatePostResponse,
  formData: FormData
): Promise<CreatePostResponse> {
  const title = formData.get('title') as string;
  const slug = formData.get('slug') as string;
  const content = formData.get('content') as string;

  await fetch('http://localhost:3001/post', {
    method: 'POST',
    body: JSON.stringify({ title, slug, content }),
    headers: {
      'Content-Type': 'application/json',
    },
  });

  return {
    message: 'Berhasil diposting!',
  };
}

bisa dilihat structur object initial state mirip dengen action.ts jadi kita bisa memberikan type seperti ini

const initialState: CreatePostResponse = {
  message: '',
};

Catatan Terkait