useSubmit

<Form> submits when a user clicks a submit button or presses enter, but sometimes you want to trigger a route's action from code instead – after a debounced keystroke in a search box, or after a window.confirm() dialog before deleting something. useSubmit() returns an imperative submit(target, options) function that does exactly what a real form submission would do, without needing an actual submit event.

target can be a form element, a FormData object, or a plain object of values, and options accepts the same things <Form> does, like method and replace. Like the rest of the hooks in this section, useSubmit() only works underneath a data router (see the Routers section).


import React, { useRef } from "react";
import { createBrowserRouter, Form, useLoaderData, useSubmit } from "react-router";
import { RouterProvider } from "react-router/dom";

const contacts = ["Alice", "Alicia", "Bob", "Bobby", "Charlie"];

const router = createBrowserRouter([
  {
    path: "/",
    element: <SearchPage />,
    loader: async ({ request }) => {
      let query = new URL(request.url).searchParams.get("q") || "";
      let results = query
        ? contacts.filter((name) => name.toLowerCase().includes(query.toLowerCase()))
        : contacts;
      return { query, results };
    },
  },
]);

export default function UseSubmitExample() {
  return <RouterProvider router={router} />;
}

function SearchPage() {
  let { query, results } = useLoaderData();
  let submit = useSubmit();
  let debounceRef = useRef(null);

  function handleChange(event) {
    let form = event.currentTarget;
    clearTimeout(debounceRef.current);
    debounceRef.current = setTimeout(() => {
      submit(form, { replace: true });
    }, 300);
  }

  return (
    <Form method="get" onChange={handleChange}>
      <input type="search" name="q" defaultValue={query} placeholder="Search contacts..." />
      <ul>
        {results.map((name) => <li key={name}>{name}</li>)}
      </ul>
    </Form>
  );
}

Every keystroke changes the input, but handleChange only calls submit() after 300ms of silence, and replace: true means each search updates the current history entry instead of piling up a new one per query. Because the <input> uses defaultValue rather than value, the browser keeps handling keystrokes natively while the query string and the loader catch up in the background.