How toAdd additional data before submitting on Remix

The Remix approach to submitting forms is to replace the <form> tag with their <Form> component and call it a day. But what happens if you need to do something async before submitting the form and attaching the result to the form data?

Ideally, you can do this in an effect and save the result in the value of a hidden input. But if this needs to happen right after the user clicks the submit button, you can't do this in an effect because the submit will occur before the effect.

The solution is to use the onSubmit prop of the <form> tag and the useSubmit() hook from Remix.

Let's say we're working on a /login route. This route has a simple form with two inputs for email and password and a submit button. We want to add a captcha to this form to prevent logins from bots.

Imagine we have a simple async function called captcha that triggers this captcha flow and returns a token if the user resolves the captcha successfully.

import { captcha } from "~/services/captcha.client";
import { useSubmit } from "@remix-run/react";

export default function Login() {
  let submit = useSubmit();

  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    // let's prevent the default event
    event.preventDefault();

    // grab the form element
    let $form = event.currentTarget;

    // get the formData from that form
    let formData = new FormData($form);

    // save the captcha token inside
    formData.set("captcha", await captcha());

    // and finally submit the form data, re-using the method and action from the form
    submit(formData, {
      method: $form.getAttribute("method") ?? $form.method,
      action: $form.getAttribute("action") ?? $form.action,
    });
  }

  return (
    <form onSubmit={handleSubmit} method="post">
      <label>Email address</label>
      <input type="email" name="email" />

      <label>Password</label>
      <input type="password" name="password" />

      <button type="submit">Log In</button>
    </form>
  );
}

And with that, we could add this extra async data right before the submit, while Remix can still handle the submit as if we used <Form> thanks to the useSubmit() hook.