Skip to content

Next.js Integration

Ctrovalidate works seamlessly with Next.js 13+ and the App Router. Because Ctrovalidate is a client-side library, you'll use the "use client" directive to create validation boundaries.


Installation

bash
npm install ctrovalidate

App Router Pattern

Forms using Ctrovalidate must be Client Components. Use the "use client" directive at the top of your component file.

Complete Example

tsx
'use client';

import { useEffect, useRef } from 'react';
import { Ctrovalidate, LogLevel } from 'ctrovalidate';

export default function ContactForm() {
  const formRef = useRef<HTMLFormElement>(null);
  const validatorRef = useRef<Ctrovalidate | null>(null);

  useEffect(() => {
    if (formRef.current && !validatorRef.current) {
      validatorRef.current = new Ctrovalidate(formRef.current, {
        realTime: true,
        logLevel: LogLevel.WARN
      });
    }

    return () => {
      validatorRef.current?.destroy();
      validatorRef.current = null;
    };
  }, []);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    if (await validatorRef.current?.validate()) {
      const formData = new FormData(formRef.current!);
      // Use Server Actions or API routes
      console.log('Form valid:', Object.fromEntries(formData));
    }
  };

  return (
    <form ref={formRef} noValidate onSubmit={handleSubmit}>
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          name="email"
          type="email"
          data-ctrovalidate-rules="required|email"
        />
        <div className="error-message" />
      </div>

      <div>
        <label htmlFor="message">Message</label>
        <textarea
          id="message"
          name="message"
          data-ctrovalidate-rules="required|minLength:10"
        />
        <div className="error-message" />
      </div>

      <button type="submit">Send Message</button>
    </form>
  );
}

Using in a Page

Import your Client Component in any page:

tsx
// app/contact/page.tsx

import ContactForm from '@/components/ContactForm';

export default function ContactPage() {
  return (
    <main>
      <h1>Contact Us</h1>
      <ContactForm />
    </main>
  );
}

Server Actions Integration

Ctrovalidate works perfectly with Next.js Server Actions for form submission:

tsx
'use client';

import { useEffect, useRef } from 'react';
import { Ctrovalidate } from 'ctrovalidate';
import { submitForm } from './actions';

export default function ServerActionForm() {
  const formRef = useRef<HTMLFormElement>(null);
  const validatorRef = useRef<Ctrovalidate | null>(null);

  useEffect(() => {
    if (formRef.current) {
      validatorRef.current = new Ctrovalidate(formRef.current, {
        realTime: true
      });
    }
    return () => validatorRef.current?.destroy();
  }, []);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    // Validate before sending to server
    if (await validatorRef.current?.validate()) {
      const formData = new FormData(formRef.current!);
      await submitForm(formData);
    }
  };

  return (
    <form ref={formRef} noValidate onSubmit={handleSubmit}>
      {/* form fields */}
    </form>
  );
}
typescript
// actions.ts
'use server';

export async function submitForm(formData: FormData) {
  // Server-side processing
  const email = formData.get('email');
  // ... save to database
}

Hydration Safety

Ctrovalidate is hydration-safe by design:

  1. useRef Initialization: The validator is created inside useEffect, which only runs on the client
  2. Server Rendering: During SSR, Next.js renders the form's static HTML with data-ctrovalidate-rules attributes
  3. Client Hydration: After hydration, useEffect fires and Ctrovalidate initializes

Why This Works

  • The data-* attributes are standard HTML and render correctly on the server
  • The validator only initializes after React hydrates the component
  • No hydration mismatches occur because validation logic runs client-side only

TypeScript Support

Full TypeScript support with proper types:

tsx
import { Ctrovalidate, LogLevel, type CtrovalidateOptions } from 'ctrovalidate';

const options: CtrovalidateOptions = {
  realTime: true,
  logLevel: LogLevel.DEBUG,
  errorClass: 'border-red-500',
  errorMessageClass: 'text-red-600 text-sm'
};

const validator = new Ctrovalidate(formElement, options);

Best Practices

1. Always Use "use client"

Mark components using Ctrovalidate as Client Components:

tsx
'use client';

import { Ctrovalidate } from 'ctrovalidate';
// ... component code

2. Initialize in useEffect

Ensure the validator only runs on the client:

tsx
useEffect(() => {
  if (formRef.current && !validatorRef.current) {
    validatorRef.current = new Ctrovalidate(formRef.current);
  }
}, []);

3. Clean Up on Unmount

Prevent memory leaks:

tsx
useEffect(() => {
  // ... initialization
  return () => {
    validatorRef.current?.destroy();
  };
}, []);

4. Validate Before Server Actions

Always validate client-side before calling Server Actions:

tsx
const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault();
  
  if (await validatorRef.current?.validate()) {
    await submitToServer(formData);
  }
};

Benefits

  • Lightweight: <5KB gzipped, smaller than form libraries
  • Server Action Ready: Validate before sending data to server
  • SEO Friendly: data-* attributes render in SSR
  • Hydration Safe: No client/server mismatches

CAUTION

Do not use Ctrovalidate in Server Components. It requires browser APIs (window, document) that are not available on the server.


Next Steps

Released under the MIT License.