Skip to Content
GuidesNext.js Integration Guide

Next.js Integration Guide

Learn how to integrate kollect with Next.js applications using App Router or Pages Router.

Table of Contents

  1. Quick Start
  2. App Router (Next.js 13+)
  3. Pages Router
  4. Server Actions
  5. API Routes
  6. Examples

Quick Start

Installation

No installation required! kollect works with standard HTML forms.

Basic Form Component

// app/components/ContactForm.tsx 'use client'; import { useState } from 'react'; export default function ContactForm() { const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault(); setStatus('loading'); const formData = new FormData(e.currentTarget); try { const response = await fetch('https://kollect.app/f/YOUR_FORM_KEY', { method: 'POST', body: formData, }); if (response.ok) { setStatus('success'); e.currentTarget.reset(); } else { setStatus('error'); } } catch (error) { setStatus('error'); } } return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="email">Email</label> <input type="email" id="email" name="email" required disabled={status === 'loading'} /> </div> <div> <label htmlFor="message">Message</label> <textarea id="message" name="message" required disabled={status === 'loading'} /> </div> <button type="submit" disabled={status === 'loading'}> {status === 'loading' ? 'Sending...' : 'Send Message'} </button> {status === 'success' && ( <p className="success">Thank you! Your message has been sent.</p> )} {status === 'error' && ( <p className="error">Something went wrong. Please try again.</p> )} </form> ); }

App Router

Client Component with useFormStatus

Use React’s useFormStatus hook for better UX:

'use client'; import { useFormStatus } from 'react-dom'; import { useState } from 'react'; function SubmitButton() { const { pending } = useFormStatus(); return ( <button type="submit" disabled={pending}> {pending ? 'Submitting...' : 'Submit'} </button> ); } export default function ContactForm() { const [message, setMessage] = useState(''); async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault(); const formData = new FormData(e.currentTarget); try { const response = await fetch('https://kollect.app/f/YOUR_FORM_KEY', { method: 'POST', body: formData, }); if (response.ok) { setMessage('Success! Thank you for your submission.'); e.currentTarget.reset(); } else { setMessage('Error! Please try again.'); } } catch (error) { setMessage('Error! Please try again.'); } } return ( <div> <form onSubmit={handleSubmit}> <input type="email" name="email" required /> <textarea name="message" required /> <SubmitButton /> </form> {message && <p>{message}</p>} </div> ); }

With TypeScript and Validation

'use client'; import { useState } from 'react'; import { z } from 'zod'; const contactSchema = z.object({ email: z.string().email('Invalid email address'), name: z.string().min(2, 'Name must be at least 2 characters'), message: z.string().min(10, 'Message must be at least 10 characters'), }); type ContactFormData = z.infer<typeof contactSchema>; export default function ContactForm() { const [errors, setErrors] = useState<Partial<Record<keyof ContactFormData, string>>>({}); const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault(); setErrors({}); setStatus('loading'); const formData = new FormData(e.currentTarget); const data = Object.fromEntries(formData); // Validate const result = contactSchema.safeParse(data); if (!result.success) { const fieldErrors: Partial<Record<keyof ContactFormData, string>> = {}; result.error.issues.forEach((issue) => { const field = issue.path[0] as keyof ContactFormData; fieldErrors[field] = issue.message; }); setErrors(fieldErrors); setStatus('idle'); return; } // Submit try { const response = await fetch('https://kollect.app/f/YOUR_FORM_KEY', { method: 'POST', body: formData, }); if (response.ok) { setStatus('success'); e.currentTarget.reset(); } else { setStatus('error'); } } catch (error) { setStatus('error'); } } return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="name">Name</label> <input type="text" id="name" name="name" /> {errors.name && <span className="error">{errors.name}</span>} </div> <div> <label htmlFor="email">Email</label> <input type="email" id="email" name="email" /> {errors.email && <span className="error">{errors.email}</span>} </div> <div> <label htmlFor="message">Message</label> <textarea id="message" name="message" /> {errors.message && <span className="error">{errors.message}</span>} </div> <button type="submit" disabled={status === 'loading'}> {status === 'loading' ? 'Sending...' : 'Send'} </button> {status === 'success' && <p>Thank you! Your message has been sent.</p>} {status === 'error' && <p>Error! Please try again.</p>} </form> ); }

Server Actions

Use Next.js Server Actions for enhanced security:

// app/actions/contact.ts 'use server'; export async function submitContact(formData: FormData) { try { const response = await fetch('https://kollect.app/f/YOUR_FORM_KEY', { method: 'POST', body: formData, }); if (!response.ok) { return { success: false, error: 'Failed to submit form' }; } return { success: true }; } catch (error) { return { success: false, error: 'Network error' }; } }
// app/components/ContactForm.tsx 'use client'; import { submitContact } from '@/app/actions/contact'; import { useState } from 'react'; export default function ContactForm() { const [message, setMessage] = useState(''); async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault(); const formData = new FormData(e.currentTarget); const result = await submitContact(formData); if (result.success) { setMessage('Thank you! Your message has been sent.'); e.currentTarget.reset(); } else { setMessage(result.error || 'Something went wrong'); } } return ( <form onSubmit={handleSubmit}> <input type="email" name="email" required /> <textarea name="message" required /> <button type="submit">Submit</button> {message && <p>{message}</p>} </form> ); }

Pages Router

Basic Implementation

// pages/contact.tsx import { useState } from 'react'; export default function ContactPage() { const [status, setStatus] = useState(''); async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault(); setStatus('Sending...'); const formData = new FormData(e.currentTarget); try { const response = await fetch('https://kollect.app/f/YOUR_FORM_KEY', { method: 'POST', body: formData, }); if (response.ok) { setStatus('Success! Thank you for your message.'); e.currentTarget.reset(); } else { setStatus('Error! Please try again.'); } } catch (error) { setStatus('Error! Please try again.'); } } return ( <div> <h1>Contact Us</h1> <form onSubmit={handleSubmit}> <input type="email" name="email" required /> <textarea name="message" required /> <button type="submit">Send</button> </form> {status && <p>{status}</p>} </div> ); }

API Routes

Proxy Through Your API

For additional processing or security:

// app/api/contact/route.ts (App Router) import { NextRequest, NextResponse } from 'next/server'; export async function POST(request: NextRequest) { const formData = await request.formData(); // Optional: Add server-side validation, rate limiting, etc. try { const response = await fetch('https://kollect.app/f/YOUR_FORM_KEY', { method: 'POST', body: formData, }); if (!response.ok) { return NextResponse.json( { error: 'Failed to submit' }, { status: 500 } ); } return NextResponse.json({ success: true }); } catch (error) { return NextResponse.json( { error: 'Network error' }, { status: 500 } ); } }
// pages/api/contact.ts (Pages Router) import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler( req: NextApiRequest, res: NextApiResponse ) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } try { const response = await fetch('https://kollect.app/f/YOUR_FORM_KEY', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(req.body), }); if (!response.ok) { return res.status(500).json({ error: 'Failed to submit' }); } return res.status(200).json({ success: true }); } catch (error) { return res.status(500).json({ error: 'Network error' }); } }

Examples

Newsletter Signup

'use client'; export default function NewsletterForm() { async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault(); const formData = new FormData(e.currentTarget); await fetch('https://kollect.app/f/YOUR_FORM_KEY', { method: 'POST', body: formData, }); alert('Thank you for subscribing!'); e.currentTarget.reset(); } return ( <form onSubmit={handleSubmit} className="flex gap-2"> <input type="email" name="email" placeholder="Enter your email" required className="flex-1" /> <button type="submit">Subscribe</button> </form> ); }

Contact Form with File Upload

'use client'; export default function ContactWithFile() { async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault(); const formData = new FormData(e.currentTarget); const response = await fetch('https://kollect.app/f/YOUR_FORM_KEY', { method: 'POST', body: formData, }); if (response.ok) { alert('Message sent!'); e.currentTarget.reset(); } } return ( <form onSubmit={handleSubmit}> <input type="email" name="email" required /> <textarea name="message" required /> <input type="file" name="attachment" /> <button type="submit">Send</button> </form> ); }

With React Hook Form

'use client'; import { useForm } from 'react-hook-form'; type FormData = { email: string; name: string; message: string; }; export default function ContactForm() { const { register, handleSubmit, formState: { errors, isSubmitting }, reset } = useForm<FormData>(); async function onSubmit(data: FormData) { const formData = new FormData(); Object.entries(data).forEach(([key, value]) => { formData.append(key, value); }); await fetch('https://kollect.app/f/YOUR_FORM_KEY', { method: 'POST', body: formData, }); reset(); alert('Form submitted!'); } return ( <form onSubmit={handleSubmit(onSubmit)}> <div> <input {...register('name', { required: 'Name is required' })} placeholder="Name" /> {errors.name && <span>{errors.name.message}</span>} </div> <div> <input {...register('email', { required: 'Email is required', pattern: { value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, message: 'Invalid email' } })} placeholder="Email" /> {errors.email && <span>{errors.email.message}</span>} </div> <div> <textarea {...register('message', { required: 'Message is required' })} placeholder="Message" /> {errors.message && <span>{errors.message.message}</span>} </div> <button type="submit" disabled={isSubmitting}> {isSubmitting ? 'Sending...' : 'Send'} </button> </form> ); }

Environment Variables

Store your form key in environment variables:

# .env.local NEXT_PUBLIC_KOLLECT_FORM_KEY=abc123xyz
const FORM_ENDPOINT = `https://kollect.app/f/${process.env.NEXT_PUBLIC_KOLLECT_FORM_KEY}`; // Use in your form await fetch(FORM_ENDPOINT, { ... });

Best Practices

  1. Client-side validation - Validate before submitting
  2. Loading states - Show feedback during submission
  3. Error handling - Handle network errors gracefully
  4. Success messages - Confirm submission to users
  5. Reset form - Clear form after successful submission
  6. Accessibility - Use proper labels and ARIA attributes
  7. Environment variables - Store form keys securely

Troubleshooting

CORS Errors

kollect includes CORS headers, but if you’re proxying through your API, ensure your API route returns proper CORS headers.

Form Not Submitting

  • Check the form key is correct
  • Verify the form is enabled in dashboard
  • Check browser console for errors

TypeScript Errors

Install types if needed:

npm install -D @types/react @types/react-dom

Next Steps

Last updated on