> ## Documentation Index
> Fetch the complete documentation index at: https://formcarry.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# React

> React sends a form to formcarry with one fetch call from the component.

It keeps the fields in state, posts them as JSON, and shows you formcarry's response. That same request carries files, a spam blocker token, and validation errors.

To try it yourself, the form's Setup page in the dashboard opens a CodeSandbox with every example on this page wired to your endpoint.

## Prerequisites

Before you begin, you'll need:

* A formcarry account. [Sign up](https://app.formcarry.com/register) is free.
* A form in the [dashboard](https://app.formcarry.com). Its endpoint is on the form's Setup page. The examples use `https://formcarry.com/s/AbC123xyz`; put yours in its place.

## The component

To send a form from React, keep the fields in state, post them as JSON, and send `Accept: application/json` so the answer comes back as JSON:

```jsx theme={null}
import { useState } from "react"

export default function ContactForm() {
  const [fields, setFields] = useState({ name: "", email: "", message: "" })
  const [status, setStatus] = useState("idle")
  const [error, setError] = useState("")

  function update(e) {
    setFields({ ...fields, [e.target.name]: e.target.value })
  }

  async function submit(e) {
    e.preventDefault()
    setStatus("sending")
    const res = await fetch("https://formcarry.com/s/AbC123xyz", {
      method: "POST",
      headers: { "Accept": "application/json", "Content-Type": "application/json" },
      body: JSON.stringify(fields),
    })
    const data = await res.json()
    if (data.code === 200) {
      setStatus("sent")
    } else {
      setStatus("error")
      setError(data.message)
    }
  }

  if (status === "sent") return <p>Thanks, we got your message.</p>

  return (
    <form onSubmit={submit}>
      <input name="name" value={fields.name} onChange={update} placeholder="Name" />
      <input name="email" type="email" value={fields.email} onChange={update} placeholder="Email" />
      <textarea name="message" value={fields.message} onChange={update} placeholder="Message" />
      <button type="submit" disabled={status === "sending"}>Send</button>
      {error && <p>{error}</p>}
    </form>
  )
}
```

The keys of the JSON body are the field names formcarry stores.

The `email` key is the visitor's address, so it becomes the reply-to address of your notification and the recipient of the auto response. A stored submission answers with `code: 200`. A refused one answers with the reason in `message`, see [What every form needs](/docs/what-every-form-needs).

## Files

To send files, build a `FormData` from the form element and leave the content type to the browser:

```jsx theme={null}
async function submit(e) {
  e.preventDefault()
  const res = await fetch("https://formcarry.com/s/AbC123xyz", {
    method: "POST",
    headers: { "Accept": "application/json" },
    body: new FormData(e.currentTarget),
  })
  const data = await res.json()
}
```

Every input in the form needs a `name`, because `FormData` reads them from the elements, and the file input needs one too. Send `Accept` and nothing else rather than adding `Content-Type` yourself, otherwise the multipart boundary is missing and the upload fails.

For several files, or for `File` objects that a dropzone library hands you, append each one under its own name:

```jsx theme={null}
const data = new FormData()
data.append("email", fields.email)
files.forEach((file, i) => data.append(`file-${i}`, file, file.name))
```

For small files you can stick with JSON and send the file as a data URL. Read it with `FileReader`, and give each file its own key (never an array):

```jsx theme={null}
const toDataUrl = (file) =>
  new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.onload = () => resolve(reader.result)
    reader.onerror = reject
    reader.readAsDataURL(file)
  })

body: JSON.stringify({ ...fields, attachment: await toDataUrl(file) })
```

Use data URLs for small files. Using them with larger files bloats the request by about a third and reaches the 50 MB limit sooner.

<Note>
  Files are stored on paid plans only. Free plans store the rest of the submission without them.
</Note>

## Spam blocker

To add a challenge:

1. Render the challenge in your component.
2. Send its token in the request body under the name formcarry expects.
3. Paste the secret key into the form's settings under Form Security.

| Challenge    | Package the demo uses       | Token field             |
| ------------ | --------------------------- | ----------------------- |
| reCAPTCHA v2 | `react-google-recaptcha`    | `g-recaptcha-response`  |
| reCAPTCHA v3 | `react-google-recaptcha-v3` | `g-recaptcha-response`  |
| hCaptcha     | `@hcaptcha/react-hcaptcha`  | `h-captcha-response`    |
| Turnstile    | `@marsidev/react-turnstile` | `cf-turnstile-response` |

With reCAPTCHA v2:

```jsx theme={null}
import ReCAPTCHA from "react-google-recaptcha"

const [token, setToken] = useState("")

<ReCAPTCHA sitekey={SITE_KEY} onChange={setToken} />

body: JSON.stringify({ ...fields, "g-recaptcha-response": token })
```

v3 doesn't have a widget: call `executeRecaptcha()` when the visitor submits and send what it returns. hCaptcha and Turnstile give you the token through their `onVerify` and `onSuccess` callbacks. While testing, add `localhost` to the challenge's allowed domains.

<Warning>
  Don't send while the token is empty. formcarry answers `403` and the visitor sees an error they could have avoided.
</Warning>

## Validation errors

A `422` carries `errors`, one entry per failing field, each with a `message`. To show each one next to its field:

```jsx theme={null}
const [fieldErrors, setFieldErrors] = useState({})

if (data.code === 422) {
  const byField = {}
  for (const field in data.errors) byField[field] = data.errors[field].message
  setFieldErrors(byField)
}

{fieldErrors.email && <p>{fieldErrors.email}</p>}
```

## Axios

The same request with Axios:

```jsx theme={null}
import axios from "axios"

try {
  const { data } = await axios.post("https://formcarry.com/s/AbC123xyz", fields, {
    headers: { Accept: "application/json" },
  })
} catch (err) {
  setError(err.response.data.message)
}
```

Leave `Content-Type` to Axios. Setting it yourself breaks the boundary on `FormData` uploads. Axios rejects on any status outside `2xx`, so formcarry's answer to a refused submission is in `err.response.data`.

## What's next

* [Field validations](/docs/features/field-validations): the rules you can set per field.
* [Spam protection](/docs/features/spam-protection): the challenges, the filter and the honeypot.
* [Email notifications](/docs/features/email-notifications): the auto response the visitor gets, keyed on `email`.

Stuck? Write to [help@formcarry.com](mailto:help@formcarry.com). Include the form id.


## Related topics

- [Next.js](/docs/frameworks/nextjs.md)
- [Thank You Pages and Redirects](/docs/features/thank-you-pages.md)
- [Framer](/docs/builders/framer.md)
- [Accept and Manage File Uploads](/docs/features/file-uploads.md)
- [Quickstart](/docs/quickstart.md)
