Skip to main content

Documentation Index

Fetch the complete documentation index at: https://grantmaster.dev/llms.txt

Use this file to discover all available pages before exploring further.

Forms & Validation

GrantMaster is a data-intensive application. We use a standardized stack to handle complex forms with dozens of fields and cross-field validation rules.

1. The Stack

  • Hook: useZodForm from @/hooks/useZodForm (wraps react-hook-form + Zod).
  • Engine: react-hook-form (RHF).
  • Validation: zod.
  • UI Components: shadcn/ui (built on Radix UI) + FormComponents.tsx (FormField, FormSection, FormActions, FormGrid).
Deprecation notice: useValidatedForm is deprecated. Always use useZodForm.

2. Standard Pattern: useZodForm

The useZodForm hook (@/hooks/useZodForm) is the standard way to build forms. It wraps react-hook-form with Zod validation and provides built-in toast notifications, error handling via handleError(), and convenience helpers.

API

const {
  form,            // Underlying RHF UseFormReturn instance
  handleSubmit,    // Submit handler for <form onSubmit={handleSubmit}>
  isSubmitting,    // Boolean — true during async submission
  isDirty,         // Boolean — form has been modified
  isValid,         // Boolean — form passes validation
  reset,           // Reset form to defaultValues
  getFieldError,   // (name) => string | undefined
  hasFieldError,   // (name) => boolean
  register,        // RHF register function
  control,         // RHF control object (for Controller)
  errors,          // Form errors object
  setValue,        // Set field values programmatically
  watch,           // Watch field values
  trigger,         // Trigger validation for specific fields
} = useZodForm({
  schema,                 // Zod schema (required)
  defaultValues,          // Default form values
  onSubmit,               // async (data) => void (required)
  onError,                // Custom error callback
  successMessage,         // Toast message on success
  errorMessagePrefix,     // Error toast prefix (default: 'Form submission failed')
  resetOnSuccess,         // Reset form after success (default: false)
  mode,                   // Validation mode (default: 'onBlur')
});

Example

import { useZodForm } from '@/hooks/useZodForm';
import { z } from 'zod';

const formSchema = z.object({
  title: z.string().min(2).max(100),
  amount: z.number().positive(),
});

export const MyForm = () => {
  const { handleSubmit, isSubmitting, control } = useZodForm({
    schema: formSchema,
    defaultValues: { title: '', amount: 0 },
    onSubmit: async (data) => {
      await saveGrant(data);
    },
    successMessage: 'Grant saved!',
  });

  return (
    <form onSubmit={handleSubmit}>
      <FormField label="Grant Title" required>
        <Input {...register('title')} placeholder="Enter title..." />
      </FormField>
      <FormField label="Amount" required>
        <Input type="number" {...register('amount', { valueAsNumber: true })} />
      </FormField>
      <Button type="submit" isLoading={isSubmitting} loadingText="Saving...">
        Save
      </Button>
    </form>
  );
};

3. Form Layout Components

Use the standardized layout components from @/components/ui/FormComponents:
  • <FormSection title="..." description="...">: Groups related fields with a heading.
  • <FormField label="..." required error="..." hint="...">: Wraps an input with label, required indicator, error/hint text, and accessible aria- attributes.
  • <FormGrid columns={2}>: Responsive grid for laying out fields.
  • <FormActions>: Right-aligned action bar for submit/cancel buttons.

4. Best Practices

Validation Source of Truth

Whenever possible, re-use the Zod schemas defined in src/schemas/. This ensures that frontend validation exactly matches the BaseService validation on the backend.

Real-time vs. On-Submit

  • Default: useZodForm defaults to mode: 'onBlur' — fields are validated when the user leaves them.
  • Critical Fields: For unique fields (like “Project Code”), use onBlur to check for duplicates in the background.
  • Eager validation: Pass mode: 'onChange' for real-time feedback (use sparingly).

Handling Large Forms

For multi-tab forms (e.g., Grant Application):
  • Split the form into smaller sub-components.
  • Pass the form object down via props or use useFormContext().
  • Only use reset() when necessary to prevent losing user progress.

Error Handling

useZodForm integrates with the centralized error handler (handleError) and toast system automatically:
  • On success: shows successMessage toast if provided.
  • On error: calls handleError() (logs to console + Sentry) and shows an error toast with errorMessagePrefix.
  • Custom error handling: pass onError callback for additional logic.

Maintenance

Update this document when:
  • Upgrading to a major new version of react-hook-form.
  • Introducing a new global validation utility.
  • Changing the standard for how error messages are displayed.