Form Handling Snippets

Common form patterns in ShipKit

Form Handling Snippets

Server Action Form

// src/app/(app)/posts/create/page.tsx
import { ValidationService } from "@/server/services/validation-service";
import { ErrorService } from "@/server/services/error-service";
import { z } from "zod";
import { revalidatePath } from "next/cache";

const schema = z.object({
  title: z.string().min(1, "Title is required"),
  content: z.string().optional(),
});

async function createPost(formData: FormData) {
  "use server";

  try {
    const data = Object.fromEntries(formData);
    const result = await ValidationService.validate(schema, data);

    if (!result.success) {
      return { error: result.error };
    }

    const post = await db?.post.create({
      data: result.data,
    });

    revalidatePath("/posts");
    return { data: post };
  } catch (error) {
    const appError = ErrorService.handleError(error);
    return { error: appError };
  }
}

export default function CreatePostPage() {
  return (
    <form action={createPost}>
      <input
        type="text"
        name="title"
        placeholder="Title"
        required
      />
      <textarea
        name="content"
        placeholder="Content"
      />
      <button type="submit">Create Post</button>
    </form>
  );
}

Client Form with React Hook Form

// src/components/forms/post-form.tsx
"use client";

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";

const formSchema = z.object({
  title: z.string().min(1, "Title is required"),
  content: z.string().optional(),
});

type FormValues = z.infer<typeof formSchema>;

export function PostForm() {
  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      title: "",
      content: "",
    },
  });

  async function onSubmit(data: FormValues) {
    try {
      const response = await fetch("/api/posts", {
        method: "POST",
        body: JSON.stringify(data),
      });

      const result = await response.json();
      if (!response.ok) {
        throw new Error(result.error?.message || "Something went wrong");
      }

      toast.success("Post created successfully");
      form.reset();
    } catch (error) {
      toast.error(error instanceof Error ? error.message : "Something went wrong");
    }
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="title"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Title</FormLabel>
              <FormControl>
                <Input {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="content"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Content</FormLabel>
              <FormControl>
                <Textarea {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <Button type="submit" disabled={form.formState.isSubmitting}>
          {form.formState.isSubmitting ? "Creating..." : "Create Post"}
        </Button>
      </form>
    </Form>
  );
}

File Upload Form

// src/components/forms/upload-form.tsx
"use client";

import { useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";

export function UploadForm() {
  const [progress, setProgress] = useState(0);
  const [isUploading, setIsUploading] = useState(false);

  async function handleSubmit(formData: FormData) {
    try {
      setIsUploading(true);
      setProgress(0);

      const response = await fetch("/api/upload", {
        method: "POST",
        body: formData,
      });

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.message || "Upload failed");
      }

      toast.success("File uploaded successfully");
    } catch (error) {
      toast.error(error instanceof Error ? error.message : "Upload failed");
    } finally {
      setIsUploading(false);
      setProgress(0);
    }
  }

  return (
    <form action={handleSubmit}>
      <input
        type="file"
        name="file"
        accept="image/*"
        required
        disabled={isUploading}
      />

      <Button type="submit" disabled={isUploading}>
        {isUploading ? "Uploading..." : "Upload"}
      </Button>

      {isUploading && (
        <Progress value={progress} className="mt-2" />
      )}
    </form>
  );
}