useSubmit
The imperative version of <Form> that lets you submit a form from code instead of a user interaction.
This hook only works in Data and Framework modes.
Signature
function useSubmit(): SubmitFunction
interface SubmitFunction {
(
target: SubmitTarget,
options?: SubmitOptions
): Promise<void>;
}
type SubmitTarget =
| HTMLFormElement
| FormData
| Record<string, any>;
Parameters
None.
Returns
A function that submits data to a route action.The data to submit. Can be:
- An
HTMLFormElement - submits the form
- A
FormData object - submits the form data
- A plain object - converted to
FormData (or JSON if encType is "application/json")
method
'get' | 'post' | 'put' | 'patch' | 'delete'
HTTP method. Defaults to "get".
The URL to submit to. Defaults to the current route.
encType
'application/x-www-form-urlencoded' | 'multipart/form-data' | 'application/json'
Encoding type for the form data. Defaults to "application/x-www-form-urlencoded".
Replace the current entry in the history stack.
State to pass to the next location.
Prevent scroll position from resetting to the top.
Wrap the state update in ReactDOM.flushSync.
Whether to navigate after submission. Set to false to use like a fetcher. Defaults to true.
When navigate is false, the key for the fetcher to use.
Usage
import { Form, useSubmit } from "react-router";
function SearchForm() {
const submit = useSubmit();
return (
<Form onChange={(e) => submit(e.currentTarget)}>
<input type="search" name="q" />
<button type="submit">Search</button>
</Form>
);
}
function Component() {
const submit = useSubmit();
return (
<button
onClick={() => {
submit(
{ intent: "delete", id: "123" },
{ method: "post" }
);
}}
>
Delete
</button>
);
}
function FileUpload() {
const submit = useSubmit();
return (
<input
type="file"
onChange={(e) => {
const formData = new FormData();
formData.append("file", e.target.files[0]);
submit(formData, {
method: "post",
encType: "multipart/form-data",
});
}}
/>
);
}
Submit JSON
function SaveButton() {
const submit = useSubmit();
const data = { name: "John", email: "[email protected]" };
return (
<button
onClick={() => {
submit(
data,
{
method: "post",
encType: "application/json",
}
);
}}
>
Save
</button>
);
}
Submit to different action
function MultiActionForm() {
const submit = useSubmit();
return (
<Form>
<input type="text" name="name" />
<button
type="button"
onClick={(e) => {
submit(e.currentTarget.form, {
method: "post",
action: "/save",
});
}}
>
Save
</button>
<button
type="button"
onClick={(e) => {
submit(e.currentTarget.form, {
method: "post",
action: "/publish",
});
}}
>
Publish
</button>
</Form>
);
}
Use as fetcher
function Component() {
const submit = useSubmit();
const deleteItem = (id: string) => {
submit(
{ id },
{
method: "post",
action: "/items/delete",
navigate: false, // Don't navigate, like a fetcher
fetcherKey: `delete-${id}`,
}
);
};
return <button onClick={() => deleteItem("123")}>Delete</button>;
}
Common Patterns
Debounced search
import { useMemo } from "react";
import { useSubmit } from "react-router";
import debounce from "lodash.debounce";
function SearchInput() {
const submit = useSubmit();
const debouncedSubmit = useMemo(
() =>
debounce((form: HTMLFormElement) => {
submit(form);
}, 300),
[submit]
);
return (
<Form onChange={(e) => debouncedSubmit(e.currentTarget)}>
<input type="search" name="q" />
</Form>
);
}
function AutoSaveForm() {
const submit = useSubmit();
const handleChange = (e: React.FormEvent<HTMLFormElement>) => {
submit(e.currentTarget, {
method: "post",
replace: true, // Don't add history entries
});
};
return (
<Form onChange={handleChange}>
<input type="text" name="title" />
<textarea name="body" />
</Form>
);
}
Confirm before submit
function DeleteButton({ itemId }) {
const submit = useSubmit();
const handleDelete = () => {
if (confirm("Are you sure?")) {
submit(
{ id: itemId },
{ method: "post", action: "/delete" }
);
}
};
return <button onClick={handleDelete}>Delete</button>;
}
Submit on interval
function HeartbeatForm() {
const submit = useSubmit();
const formRef = useRef<HTMLFormElement>(null);
useEffect(() => {
const interval = setInterval(() => {
if (formRef.current) {
submit(formRef.current, {
method: "post",
replace: true,
});
}
}, 5000);
return () => clearInterval(interval);
}, [submit]);
return (
<Form ref={formRef}>
<input type="hidden" name="heartbeat" value="true" />
</Form>
);
}
Programmatic logout
function LogoutButton() {
const submit = useSubmit();
return (
<button
onClick={() => {
submit(null, {
method: "post",
action: "/logout",
});
}}
>
Logout
</button>
);
}
Submit with loading state
function SaveButton({ data }) {
const submit = useSubmit();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
return (
<button
onClick={() => submit(data, { method: "post" })}
disabled={isSubmitting}
>
{isSubmitting ? "Saving..." : "Save"}
</button>
);
}
function BulkActions({ items }) {
const submit = useSubmit();
const deleteAll = () => {
items.forEach((item) => {
submit(
{ id: item.id },
{
method: "post",
action: "/delete",
navigate: false,
fetcherKey: `delete-${item.id}`,
}
);
});
};
return <button onClick={deleteAll}>Delete All</button>;
}
Encoding Types
submit(
{ name: "John", email: "[email protected]" },
{ method: "post" }
);
// Sent as: name=John&email=john%40example.com
const formData = new FormData();
formData.append("file", fileInput.files[0]);
submit(formData, {
method: "post",
encType: "multipart/form-data",
});
application/json
submit(
{
user: {
name: "John",
settings: { theme: "dark" },
},
},
{
method: "post",
encType: "application/json",
}
);
// Sent as: {"user":{"name":"John","settings":{"theme":"dark"}}}
Type Safety
interface FormData {
name: string;
email: string;
}
function Component() {
const submit = useSubmit();
const handleSubmit = (data: FormData) => {
submit(data, {
method: "post",
action: "/api/users",
});
};
}