ignitionstack.pro v1.0 is out! Read the announcement →
Skip to Content

Form Components

Production-ready forms that cover the two main conversion funnels in ignitionstack.pro (leads and admin CRUD). Every form is fully typed, uses the shared UI primitives, and is wired to the appropriate Server Actions for persistence and telemetry.

Form Error Components

FormError

Displays form-level error messages.

import { FormError } from '@/components/ui/form-error' <FormError error={error} /> // With custom styling <FormError error="Something went wrong" className="mb-4" />

FieldError

Displays field-level validation errors.

import { FieldError } from '@/components/ui/form-error' <FieldError error={emailError} />

Submit Buttons

SubmitButton

Button with loading state for form submissions.

import { SubmitButton } from '@/components/ui/submit-button' <form action={createPost}> {/* form fields */} <SubmitButton>Create Post</SubmitButton> </form> // With custom loading text <SubmitButton loadingText="Creating..."> Create Post </SubmitButton> // Disabled state <SubmitButton disabled={!isValid}> Submit </SubmitButton>

Pre-configured Buttons

import { CreateButton, UpdateButton, DeleteButton } from '@/components/ui/submit-button' // Create action <CreateButton>Add Product</CreateButton> // Update action <UpdateButton>Save Changes</UpdateButton> // Delete action (with confirmation styling) <DeleteButton>Remove Item</DeleteButton>

Custom Hooks

useFormField

Field-level state and validation hook.

'use client' import { useFormField } from '@/hooks' export default function MyForm() { const email = useFormField('', { validator: 'email' }) const name = useFormField('', { required: true }) return ( <form> <input {...email.inputProps} placeholder="Email" /> {email.error && <span>{email.error}</span>} <input {...name.inputProps} placeholder="Name" /> {name.error && <span>{name.error}</span>} </form> ) }

useAsyncAction

Manage async operations with loading and error states.

'use client' import { useAsyncAction } from '@/hooks' import { createPost } from '@/server/actions/posts' export default function CreatePostForm() { const { execute, isLoading, error } = useAsyncAction(createPost) const handleSubmit = async (formData: FormData) => { const result = await execute(formData) if (result.success) { // Handle success } } return ( <form action={handleSubmit}> {error && <FormError error={error} />} <SubmitButton disabled={isLoading}> {isLoading ? 'Creating...' : 'Create Post'} </SubmitButton> </form> ) }

useOptimisticAction

Optimistic updates with automatic rollback.

'use client' import { useOptimisticAction } from '@/hooks' import { toggleLike } from '@/server/actions/likes' export default function LikeButton({ post, initialLiked }) { const { execute, optimisticValue } = useOptimisticAction( toggleLike, initialLiked, (current) => !current // Optimistic update ) return ( <button onClick={() => execute(post.id)}> {optimisticValue ? 'Unlike' : 'Like'} </button> ) }

ContactForm

Complete contact form with validation and analytics.

import ContactForm from '@/components/contact/contact-form' // In your page <ContactForm />

Features

Implementation

'use client' import { useFormField, useAsyncAction } from '@/hooks' import { sendContactEmail } from '@/server/actions/contact' import { FormError, SubmitButton } from '@/components/ui' import TextInput from '@/components/ui/text-input' import TextArea from '@/components/ui/text-area' export default function ContactForm() { const name = useFormField('', { required: true }) const email = useFormField('', { validator: 'email' }) const message = useFormField('', { required: true, minLength: 10 }) const { execute, isLoading, error, success } = useAsyncAction( sendContactEmail ) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!name.isValid || !email.isValid || !message.isValid) { return } const formData = new FormData() formData.append('name', name.value) formData.append('email', email.value) formData.append('message', message.value) const result = await execute(formData) if (result.success) { name.reset() email.reset() message.reset() } } if (success) { return ( <div className="text-center p-8"> <h3>Thank you!</h3> <p>We'll get back to you soon.</p> </div> ) } return ( <form onSubmit={handleSubmit} className="space-y-4"> <FormError error={error} /> <div> <TextInput {...name.inputProps} placeholder="Your name" /> <FieldError error={name.error} /> </div> <div> <TextInput {...email.inputProps} type="email" placeholder="your@email.com" /> <FieldError error={email.error} /> </div> <div> <TextArea {...message.inputProps} placeholder="Your message..." rows={5} /> <FieldError error={message.error} /> </div> <SubmitButton disabled={isLoading}> {isLoading ? 'Sending...' : 'Send Message'} </SubmitButton> </form> ) }

Admin CRUD Forms

Create Form Pattern

Define Server Action

// server/actions/products.ts 'use server' import { createAdminClient } from '@/lib/supabase/server' import { ActionResult } from '@/lib/action-result' import { withAdminAuth } from '@/lib/admin-action-wrapper' export const createProduct = withAdminAuth(async ( formData: FormData ): Promise<ActionResult<Product>> => { const supabase = createAdminClient() const { data, error } = await supabase .from('products') .insert({ name: formData.get('name') as string, price: Number(formData.get('price')), description: formData.get('description') as string, }) .select() .single() if (error) { return { success: false, error: error.message } } return { success: true, data } })

Create Form Component

'use client' import { useFormField, useAsyncAction } from '@/hooks' import { createProduct } from '@/server/actions/products' import { CreateButton, FormError, FieldError } from '@/components/ui' export default function CreateProductForm() { const name = useFormField('', { required: true }) const price = useFormField('', { required: true }) const description = useFormField('') const { execute, isLoading, error } = useAsyncAction(createProduct) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() const formData = new FormData() formData.append('name', name.value) formData.append('price', price.value) formData.append('description', description.value) const result = await execute(formData) if (result.success) { // Redirect or show success } } return ( <form onSubmit={handleSubmit}> <FormError error={error} /> {/* Form fields */} <CreateButton disabled={isLoading}> Create Product </CreateButton> </form> ) }

Form Validation with Zod

For complex validation, use Zod schemas:

import { z } from 'zod' const ProductSchema = z.object({ name: z.string().min(1, 'Name is required').max(100), price: z.number().positive('Price must be positive'), description: z.string().max(1000).optional(), }) // In server action export async function createProduct(formData: FormData) { const result = ProductSchema.safeParse({ name: formData.get('name'), price: Number(formData.get('price')), description: formData.get('description'), }) if (!result.success) { return { success: false, error: result.error.errors[0].message } } // Continue with validated data const { name, price, description } = result.data }

Zod validation runs on the server, providing an additional security layer beyond client-side validation.

Best Practices

  1. Always validate server-side - Never trust client input
  2. Use optimistic updates - For better UX on slow networks
  3. Handle all error states - Show meaningful error messages
  4. Use proper input types - type="email", type="tel", etc.
  5. Add loading indicators - Always show submission progress
  6. Reset forms on success - Clear fields after successful submission

Next Steps