npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

use-contact-form

v1.1.0

Published

A lightweight React hook for handling contact form submissions with your backend

Readme

use-contact-form

A lightweight, TypeScript-first React hook for handling contact form submissions with your own backend server.

💡 You control the backend, we handle the frontend complexity.

npm version License: MIT

🚀 Live Demo | 📚 Full Documentation

Why use-contact-form?

This hook doesn't send emails directly. Instead, it provides a robust interface for submitting to YOUR backend, where YOU control the email service (Resend, SendGrid, Nodemailer, etc.), keep API keys secure, and add spam protection.

Think of it as React Query for contact forms - it handles states, retries, errors, and cancellation while you focus on your API.

Features

TypeScript Support - Fully typed with generics for your data structures
Service Agnostic - Works with any backend (Next.js, Express, serverless)
Retry Logic - Configurable retry with exponential backoff
Request Cancellation - Cancel pending requests with AbortController
Built-in Validation - Optional client-side validation helpers
Loading States - Track loading, error, success states
Lightweight - Zero dependencies (except React)
Flexible - Customizable headers, timeout, transformers

Installation

# npm
npm install use-contact-form

# yarn
yarn add use-contact-form

# pnpm
pnpm add use-contact-form

Quick Start

Note: You need a backend endpoint to send emails. See Backend Examples below.

Frontend (React)

import { useContactForm } from 'use-contact-form';

function ContactForm() {
  const { sendEmail, loading, error, success } = useContactForm({
    endpoint: '/api/contact',
  });

  const handleSubmit = async (e) => {
    e.preventDefault();
    await sendEmail({
      name: 'John Doe',
      email: '[email protected]',
      message: 'Hello!',
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Your form fields */}
      <button type="submit" disabled={loading}>
        {loading ? 'Sending...' : 'Send'}
      </button>
      {error && <p>Error: {error.message}</p>}
      {success && <p>Message sent successfully!</p>}
    </form>
  );
}

Backend (Next.js Example)

// app/api/contact/route.ts
import { NextResponse } from 'next/server';

export async function POST(req: Request) {
  const { name, email, message } = await req.json();
  
  // Your validation, spam protection, etc.
  
  // Send email using your preferred service
  // (Resend, SendGrid, Nodemailer, etc.)
  
  return NextResponse.json({ success: true });
}

API Reference

useContactForm(options)

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | endpoint | string | required | API endpoint to send data to | | method | 'POST' \| 'PUT' \| 'PATCH' | 'POST' | HTTP method | | headers | Record<string, string> | {} | Custom headers | | timeout | number | 10000 | Request timeout in milliseconds | | retries | number | 0 | Number of retry attempts | | retryDelay | number | 1000 | Base delay between retries (ms) | | onSuccess | (data) => void | undefined | Success callback | | onError | (error) => void | undefined | Error callback | | transformData | (data) => any | undefined | Transform data before sending | | validate | (data) => ValidationResult | undefined | Client-side validation |

Returns

| Property | Type | Description | |----------|------|-------------| | sendEmail | (data) => Promise<TResponse> | Function to send form data | | loading | boolean | Whether request is in progress | | error | ContactFormError \| null | Error object if failed | | data | TResponse \| null | Response data from server | | success | boolean | Whether submission succeeded | | reset | () => void | Reset state (error, data, success) | | cancel | () => void | Cancel current request |

Advanced Usage

Email Templates (HTML/Text)

You can use built-in email templates to render consistent emails on your backend.

import { renderHtmlTemplate, renderTextTemplate } from 'use-contact-form';

// Inside your backend route/handler
const html = renderHtmlTemplate({
  name: 'John',
  email: '[email protected]',
  message: 'Hello there!',
  subject: 'Contact from website',
});

const text = renderTextTemplate({
  name: 'John',
  email: '[email protected]',
  message: 'Hello there!',
});

// Send via your email service
// e.g. Resend/Nodemailer body: { html, text }

Options:

const html = renderHtmlTemplate(data, {
  theme: 'minimal' | 'branded' | 'dark',
  brandColor: '#4F46E5',
  logoUrl: 'https://your.cdn/logo.svg',
  footerText: 'Sent from my website',
});

All user content is HTML-escaped by default.

With TypeScript

import { useContactForm, ContactFormData, ContactFormResponse } from 'use-contact-form';

interface MyFormData extends ContactFormData {
  phone?: string;
  company?: string;
}

interface MyResponse extends ContactFormResponse {
  ticketId: string;
}

const { sendEmail, loading } = useContactForm<MyFormData, MyResponse>({
  endpoint: '/api/contact',
  onSuccess: (data) => {
    console.log('Ticket created:', data.ticketId);
  },
});

With Validation

import { useContactForm, validateContactForm } from 'use-contact-form';

const { sendEmail } = useContactForm({
  endpoint: '/api/contact',
  validate: validateContactForm, // Built-in validator
  // Or use custom validation:
  validate: (data) => {
    const errors: Record<string, string[]> = {};
    if (data.name.length < 2) {
      errors.name = ['Name must be at least 2 characters'];
    }
    return {
      valid: Object.keys(errors).length === 0,
      errors,
    };
  },
});

With Retry Logic

const { sendEmail } = useContactForm({
  endpoint: '/api/contact',
  retries: 3, // Retry 3 times
  retryDelay: 1000, // Start with 1s delay (exponential backoff)
  timeout: 15000, // 15 second timeout
});

With Custom Headers

const { sendEmail } = useContactForm({
  endpoint: 'https://api.example.com/contact',
  headers: {
    'Authorization': `Bearer ${token}`,
    'X-API-Key': 'your-api-key',
  },
});

With Data Transformation

const { sendEmail } = useContactForm({
  endpoint: '/api/contact',
  transformData: (data) => ({
    ...data,
    timestamp: new Date().toISOString(),
    source: 'website',
  }),
});

With Request Cancellation

const { sendEmail, cancel, loading } = useContactForm({
  endpoint: '/api/contact',
});

// Cancel if user navigates away
useEffect(() => {
  return () => {
    if (loading) cancel();
  };
}, [loading, cancel]);

Backend Setup (Step-by-step)

Option A: Next.js + Resend (recommended)

  1. Install dependencies
npm install resend
  1. Create API route app/api/contact/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { Resend } from 'resend';

const resend = new Resend(process.env.RESEND_API_KEY);

// CORS (allow all; restrict in production if needed)
const cors = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type'
};

export async function OPTIONS() {
  return NextResponse.json({}, { headers: cors });
}

export async function POST(req: NextRequest) {
  try {
    const { name, email, message, subject } = await req.json();

    if (!name || !email || !message) {
      return NextResponse.json(
        { success: false, message: 'Missing required fields' },
        { status: 400, headers: cors }
      );
    }

    const data = await resend.emails.send({
      from: process.env.EMAIL_FROM || 'Contact Form <[email protected]>',
      to: process.env.EMAIL_TO || '[email protected]',
      reply_to: email,
      subject: subject || `Contact from ${name}`,
      html: `<p><strong>From:</strong> ${name} (${email})</p><p>${message}</p>`
    });

    return NextResponse.json({ success: true, data }, { headers: cors });
  } catch (e) {
    return NextResponse.json({ success: false, message: 'Server error' }, { status: 500, headers: cors });
  }
}
  1. Set environment variables
# .env
RESEND_API_KEY=your_resend_api_key
EMAIL_FROM="Contact Form <[email protected]>"
[email protected]
  1. Test locally
curl -X POST http://localhost:3000/api/contact \
  -H 'Content-Type: application/json' \
  -d '{"name":"John","email":"[email protected]","message":"Hello"}'
  1. Deploy to Vercel (detailed)

    1. Push your project to GitHub (or GitLab/Bitbucket)
    2. Go to Vercel and click "New Project" → import your repo
    3. Framework Preset: Next.js (auto-detected)
    4. Environment Variables (add exactly as in your .env):
      • RESEND_API_KEY
      • EMAIL_FROM (e.g. Contact Form <[email protected]> or your verified domain)
      • EMAIL_TO (where you want to receive messages)
    5. Click "Deploy"
    6. After deploy, verify endpoint works:
    curl -X POST https://<your-vercel-app>.vercel.app/api/contact \
      -H 'Content-Type: application/json' \
      -d '{"name":"John","email":"[email protected]","message":"Hello"}'
    1. In your frontend, set the hook endpoint to the deployed API URL:
    const { sendEmail } = useContactForm({
      endpoint: 'https://<your-vercel-app>.vercel.app/api/contact',
    });

    Notes for Resend:

    • Free testing may only allow sending to your account email until you verify a domain.
    • To send to arbitrary recipients, verify a domain at Resend and use a from address on that domain.

Option B: Next.js + Nodemailer (SMTP/Gmail)

  1. Install dependencies
npm install nodemailer
  1. Create API route app/api/contact/route.ts (SMTP)
import { NextRequest, NextResponse } from 'next/server';
import nodemailer from 'nodemailer';

const cors = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type'
};

export async function OPTIONS() { return NextResponse.json({}, { headers: cors }); }

export async function POST(req: NextRequest) {
  const { name, email, message, subject } = await req.json();
  if (!name || !email || !message) return NextResponse.json({ success: false }, { status: 400, headers: cors });

  const transporter = nodemailer.createTransport({
    host: process.env.SMTP_HOST,
    port: parseInt(process.env.SMTP_PORT || '587'),
    secure: process.env.SMTP_SECURE === 'true',
    auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
  });

  const info = await transporter.sendMail({
    from: process.env.EMAIL_FROM || process.env.SMTP_USER,
    to: process.env.EMAIL_TO || process.env.SMTP_USER,
    replyTo: email,
    subject: subject || `Contact from ${name}`,
    text: message,
  });

  return NextResponse.json({ success: true, id: info.messageId }, { headers: cors });
}
  1. Environment variables
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
[email protected]
SMTP_PASS=your_app_password   # Use Gmail App Passwords
[email protected]
[email protected]

Option C: Express + Nodemailer

npm install express nodemailer cors
const express = require('express');
const cors = require('cors');
const nodemailer = require('nodemailer');

const app = express();
app.use(cors());
app.use(express.json());

const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST,
  port: process.env.SMTP_PORT,
  auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
});

app.post('/api/contact', async (req, res) => {
  const { name, email, message, subject } = req.body;
  if (!name || !email || !message) return res.status(400).json({ success: false });

  await transporter.sendMail({
    from: process.env.EMAIL_FROM,
    to: process.env.EMAIL_TO,
    replyTo: email,
    subject: subject || `Contact from ${name}`,
    text: message,
  });

  res.json({ success: true });
});

app.listen(3000);

CORS notes

  • Local testing (file:// or different ports) requires CORS. The examples above include permissive * headers.
  • For production, restrict origins to your domain(s).

Vercel troubleshooting

  • 400 "Missing required fields": ensure name, email, and message are in the POST body
  • 500 with Resend: confirm RESEND_API_KEY, EMAIL_FROM, and EMAIL_TO are set in Vercel Project → Settings → Environment Variables
  • Resend 403 "testing emails only": verify a domain in Resend and update EMAIL_FROM to use that domain
  • CORS error from browser: confirm your route includes an OPTIONS handler and returns Access-Control-Allow-* headers on all responses
  • Receiving but wrong inbox: check EMAIL_TO in Vercel env

Backend Examples

Next.js with Resend

// app/api/contact/route.ts
import { Resend } from 'resend';

const resend = new Resend(process.env.RESEND_API_KEY);

export async function POST(req: Request) {
  const { name, email, message } = await req.json();

  await resend.emails.send({
    from: 'Contact Form <[email protected]>',
    to: '[email protected]',
    replyTo: email,
    subject: `Contact from ${name}`,
    html: `<p><strong>From:</strong> ${name} (${email})</p><p>${message}</p>`,
  });

  return Response.json({ success: true });
}

Express with Nodemailer

const express = require('express');
const nodemailer = require('nodemailer');

const app = express();
app.use(express.json());

const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST,
  port: process.env.SMTP_PORT,
  auth: {
    user: process.env.SMTP_USER,
    pass: process.env.SMTP_PASS,
  },
});

app.post('/api/contact', async (req, res) => {
  const { name, email, message } = req.body;

  await transporter.sendMail({
    from: process.env.EMAIL_FROM,
    to: process.env.EMAIL_TO,
    replyTo: email,
    subject: `Contact from ${name}`,
    text: message,
  });

  res.json({ success: true });
});

app.listen(3000);

AWS Lambda with SES

import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';

const ses = new SESClient({ region: 'us-east-1' });

export const handler = async (event) => {
  const { name, email, message } = JSON.parse(event.body);

  await ses.send(new SendEmailCommand({
    Source: '[email protected]',
    Destination: { ToAddresses: ['[email protected]'] },
    Message: {
      Subject: { Data: `Contact from ${name}` },
      Body: { Text: { Data: message } },
    },
  }));

  return {
    statusCode: 200,
    body: JSON.stringify({ success: true }),
  };
};

Security Best Practices

  1. Never expose API keys in frontend code - Always use a backend server
  2. Validate on the server - Client-side validation is for UX, not security
  3. Rate limit your API - Prevent spam and abuse
  4. Use CAPTCHA - For public forms, consider adding reCAPTCHA or similar
  5. Sanitize inputs - Always sanitize before sending emails
  6. Use CORS properly - Restrict which domains can call your API

Error Handling

The hook returns detailed error information:

interface ContactFormError {
  message: string;        // Human-readable error message
  code?: string;          // Error code (e.g., 'VALIDATION_ERROR')
  status?: number;        // HTTP status code
  errors?: Record<string, string[]>; // Field-specific errors
}

Example usage:

{error && (
  <div>
    <p>{error.message}</p>
    {error.errors && Object.entries(error.errors).map(([field, messages]) => (
      <p key={field}>{field}: {messages.join(', ')}</p>
    ))}
  </div>
)}

Testing

import { renderHook, act } from '@testing-library/react-hooks';
import { useContactForm } from 'use-contact-form';

test('sends email successfully', async () => {
  const { result } = renderHook(() =>
    useContactForm({ endpoint: '/api/contact' })
  );

  await act(async () => {
    await result.current.sendEmail({
      name: 'Test',
      email: '[email protected]',
      message: 'Hello',
    });
  });

  expect(result.current.success).toBe(true);
});

Complete Examples

Check out the repository for full working examples:

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

FAQ

Q: Does this package send emails directly?
A: No. You implement your own backend API, which gives you full control over email services, API keys, and security.

Q: Which email services are supported?
A: Any! Resend, SendGrid, Nodemailer, AWS SES, Mailgun - use whatever you prefer on your backend.

Q: Can I use this with React Server Components?
A: This is a client-side hook (uses useState). For RSC, use Server Actions or API routes as your backend.

License

MIT © Sreekar Siddula

Support


Made with ❤️ by Sreekar Siddula