Form and useSubmit

<Form> from react-router renders a real <form> and intercepts its submit event. Instead of a full page load, the router serializes the fields into a FormData, calls the matching route's action, revalidates the loaders and re-renders — client-side, but with the browser's own semantics preserved.

A Form posting to its own route's actionHTMLLive
<Form method="post" action="/products/new" replace>
  <label>Name <input name="name" /></label>
  <label>Price <input name="price" type="number" step="0.01" /></label>
  <button type="submit" name="intent" value="save">Save</button></Form>

method defaults to get, which is no mutation: the fields become a query string on the current route and the loaders re-run — what a search form wants. Any other method routes to the action. action names the route to submit to, replace swaps the history entry instead of pushing one, encType selects application/x-www-form-urlencoded (the default), multipart/form-data or application/json, and navigate={false} turns the submission into a fetcher (useFetcher). The fields are real DOM inputs needing no React 7,897 state: name is the contract with request.formData(), and a submit button carrying its own name/value — the intent pattern above — lets one form drive several branches of an action.

useSubmit() returns an imperative submit(target, options) — target is a form element, a FormData, a URLSearchParams, a plain object or null — for submissions that no button triggers.

A search box that submits 300 ms after the last keystrokeJavaScript
function Search() {
  const submit = useSubmit(), timer = useRef(null);
  function handleChange(event) {
    const form = event.currentTarget;
    clearTimeout(timer.current);
    timer.current = setTimeout(() => submit(form, { replace: true }), 300);
  }
  return <Form method="get" onChange={handleChange}>
    <input type="search" name="q" defaultValue={useLoaderData().q} /></Form>;
}

The loader reads new URL(request.url).searchParams.get('q') and returns the matches, so the query lives in the URL and the page is shareable. replace: true keeps 20 keystrokes from becoming 20 history entries, and defaultValue rather than value leaves typing to the browser while the loader catches up. Otherwise prefer <Form>: it keeps working, as a plain HTML form, before JavaScript has loaded.