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>
);
}