Skip to content

How to build a contact form in React Native (and Expo) that sends email

A React Native contact form that emails you every message, with a sending state, bot protection and no backend. Works in Expo and bare React Native.

Updated:

You’ll add a contact form to a React Native or Expo app with the @frontmail/react-native package: a provider, a useSendEmail hook that tracks the sending state, and a ready-made Turnstile component against bots. The email goes out through your own mailbox.

You need: a free Frontmail account, an Expo or React Native project and about ten minutes.

1. Connect your mailbox and create the template

  1. Email services → Add service: connect Gmail, Outlook, SMTP or a transactional provider, tick Make default, Verify & save.
  2. Templates → From gallery → Contact form → Use template. Required parameters: name, email, message; optional: phone, topic.
  3. Copy the template ID (tpl_…) from the Snippets tab.

2. Allow mobile apps

  • Security → Mobile apps → Allow mobile apps → Save. Apps send no Origin header, so without this switch the API answers origin_not_allowed.
  • Template → Settings → Protection → Require CAPTCHA (Turnstile). The app uses Frontmail’s shared Turnstile widget; you don’t need Cloudflare keys.

3. Install

npx expo install @frontmail/react-native @react-native-async-storage/async-storage react-native-webview

In a bare React Native project use npm i with the same packages and run npx pod-install.

4. Build the form

import { useRef, useState } from 'react';
import { Button, Text, TextInput, View } from 'react-native';
import { FrontmailProvider, TurnstileWebView, useSendEmail, type TurnstileWebViewHandle } from '@frontmail/react-native';

const options = { publicKey: 'pk_…' }; // keep the object stable

export default function App() {
  return (
    <FrontmailProvider options={options}>
      <ContactForm />
    </FrontmailProvider>
  );
}

function ContactForm() {
  const { send, status, error } = useSendEmail(null, 'tpl_…'); // null = default service
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [message, setMessage] = useState('');
  const [token, setToken] = useState<string>();
  const turnstile = useRef<TurnstileWebViewHandle>(null);

  const onSend = async () => {
    await send({ name, email, message }, { turnstileToken: token });
    setToken(undefined);
    turnstile.current?.reset(); // a token works only once
  };

  if (status === 'sent' || status === 'held') return <Text>Thank you! We’ll get back to you soon.</Text>;

  return (
    <View style={{ gap: 12, padding: 16 }}>
      <TextInput placeholder="Name" value={name} onChangeText={setName} />
      <TextInput placeholder="Email" value={email} onChangeText={setEmail} keyboardType="email-address" autoCapitalize="none" />
      <TextInput placeholder="Message" value={message} onChangeText={setMessage} multiline />
      <TurnstileWebView ref={turnstile} onToken={setToken} />
      <Button title="Send" disabled={!token || status === 'sending'} onPress={onSend} />
      {error && <Text>{error.message}</Text>}
    </View>
  );
}

send never throws: it resolves with the result or undefined, and status moves from idle to sending and then sent, held or error. Retries after network errors reuse one idempotency key, so a user on a train never sends you the same message twice.

5. Test it

Start the app (npx expo start), fill in the form and tap Send. The email lands in your inbox and in History in the dashboard.

  • origin_not_allowed – Allow mobile apps is off or not saved.
  • captcha_required – the Turnstile check hasn’t finished yet; the button stays disabled until it has a token.
  • invalid_template_params – a required field is empty or the email is invalid.

held means the message was accepted while your credits were used up. It goes out automatically as soon as you top up – nothing is dropped.

Next steps

  • Add limitRate: { id: 'contact', throttle: 30_000 } to the provider options to allow one message per 30 seconds per device.
  • Send a confirmation to the user with the Auto-reply template from the gallery.
All tutorials