MyApp

Getting Started

Introduction

User Interface

Button.tsxCard.tsxDialog.tsxMarquee.tsxBentoGrid.tsx

Feature Components

PricingCard.tsxTestimonials.tsxTotalBuyers.tsxFaq.tsxEvent.tsx

Utility Components

SvgFinder.tsxCustomIcons.tsImageWithFallback.tsx

Patterns

Layout PatternsForm PatternsData Fetching PatternsError Handling Patterns
MyApp

Form Patterns

Common form patterns using React Hook Form, Zod validation, and Clerk authentication.

Form patterns in Plainform use React Hook Form for state management, Zod for validation, and integrate seamlessly with Clerk for authentication flows.

Form Stack

  • React Hook Form: Form state and validation
  • Zod: Schema validation with TypeScript inference
  • @hookform/resolvers: Connects Zod schemas to React Hook Form
  • Clerk: Authentication API integration
  • Sonner: Toast notifications for errors

Basic Form Pattern

Setup

@/components/MyForm.tsx
'use client';

import { useForm, SubmitHandler } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Input } from '@/components/ui/Input';
import { Label } from '@/components/ui/Label';
import { Button } from '@/components/ui/Button';

// Define schema
const formSchema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
});

type FormData = z.infer<typeof formSchema>;

export function MyForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<FormData>({
    mode: 'onTouched',
    resolver: zodResolver(formSchema),
  });

  const onSubmit: SubmitHandler<FormData> = async (data) => {
    // Handle form submission
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
      <div className="flex flex-col gap-2">
        <Label htmlFor="email">Email</Label>
        <Input
          {...register('email')}
          id="email"
          type="email"
          placeholder="you@example.com"
          errorMessage={errors.email?.message}
          isInvalid={!!errors.email}
          disabled={isSubmitting}
        />
      </div>

      <div className="flex flex-col gap-2">
        <Label htmlFor="password">Password</Label>
        <Input
          {...register('password')}
          id="password"
          type="password"
          placeholder="●●●●●●●●"
          errorMessage={errors.password?.message}
          isInvalid={!!errors.password}
          disabled={isSubmitting}
        />
      </div>

      <Button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Submit'}
      </Button>
    </form>
  );
}

Clerk Authentication Pattern

Sign In Form

@/components/user/SignInForm.tsx
'use client';

import { useSignIn } from '@clerk/nextjs';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';
import { ClerkAPIError } from '@clerk/types';

export function SignInForm() {
  const { signIn, setActive, isLoaded } = useSignIn();
  const router = useRouter();
  const {
    register,
    handleSubmit,
    setError,
    formState: { errors, isSubmitting },
  } = useForm<FormData>({
    resolver: zodResolver(signInSchema),
  });

  const onSubmit: SubmitHandler<FormData> = async (data) => {
    if (!isLoaded) return;

    try {
      const result = await signIn.create({
        identifier: data.email,
        password: data.password,
      });

      if (result.status === 'complete') {
        await setActive({ session: result.createdSessionId });
        router.push('/');
      }
    } catch (err: any) {
      err.errors.forEach((error: ClerkAPIError) => {
        const paramName = error.meta?.paramName as keyof FormData;
        if (paramName && error.longMessage) {
          setError(paramName, {
            type: 'manual',
            message: error.longMessage,
          });
        } else {
          toast.error(error.message);
        }
      });
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* Form fields */}
    </form>
  );
}

Validation Schemas

Store schemas in validationSchemas/ directory:

@/validationSchemas/authSchemas.ts
import { z } from 'zod';

export const signInSchema = z.object({
  identifier: z.string().email('Please enter a valid email address'),
  password: z.string().min(1, 'Password is required'),
});

export const signUpSchema = z.object({
  emailAddress: z.string().email('Please enter a valid email address'),
  password: z
    .string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/[A-Z]/, 'Password must contain an uppercase letter')
    .regex(/[0-9]/, 'Password must contain a number'),
  firstName: z.string().min(1, 'First name is required'),
  lastName: z.string().min(1, 'Last name is required'),
});

Error Handling Patterns

Field-Level Errors

Field Error Display
<Input
  {...register('email')}
  errorMessage={errors.email?.message}
  isInvalid={!!errors.email}
/>

API Errors with Toast

API Error Handling
catch (err: any) {
  if (err.errors) {
    err.errors.forEach((error: ClerkAPIError) => {
      const field = error.meta?.paramName as keyof FormData;
      if (field) {
        setError(field, { message: error.longMessage });
      } else {
        toast.error(error.message);
      }
    });
  }
}

Form Components

StepHeader

@/components/user/StepHeader.tsx
interface IStepHeader {
  title: string;
  description: string;
}

export function StepHeader({ title, description }: IStepHeader) {
  return (
    <div className="flex flex-col gap-2">
      <h1 className="text-2xl font-bold">{title}</h1>
      <p className="text-neutral-foreground">{description}</p>
    </div>
  );
}

StepFooter

@/components/user/StepFooter.tsx
interface IStepFooter {
  title: string;
  buttonText: string;
  href: string;
}

export function StepFooter({ title, buttonText, href }: IStepFooter) {
  return (
    <p className="text-sm text-neutral-foreground">
      {title}{' '}
      <Link href={href} className="text-foreground font-medium">
        {buttonText}
      </Link>
    </p>
  );
}

Loading States

Submit Button with Loader
import { BeatLoader } from 'react-spinners';

<Button disabled={isSubmitting} type="submit">
  {isSubmitting && (
    <BeatLoader size={5} className="[&>span]:!bg-foreground" />
  )}
  {isSubmitting ? 'Signing in...' : 'Sign In'}
</Button>

Related

  • React Hook Form - Form library documentation
  • Zod - Schema validation
  • Clerk Authentication - Auth integration
  • Input.tsx - Input component

How is this guide ?

Last updated on

Layout Patterns

Reusable layout components for consistent page structure and spacing.

Data Fetching Patterns

Server-side data fetching patterns using lib functions that call API routes for clean separation of concerns.

On this page

Form Stack
Basic Form Pattern
Setup
Clerk Authentication Pattern
Sign In Form
Validation Schemas
Error Handling Patterns
Field-Level Errors
API Errors with Toast
Form Components
StepHeader
StepFooter
Loading States
Related