useSubmit
The useSubmit hook is designed for mutating data on the server. It provides a powerful and streamlined way to
handle submissions, such as creating, updating, or deleting resources. It integrates with Hyper Fetch's core systems
like the Dispatcher and Cache to manage request states and data
consistency.
- Simplified mutations: Easily trigger data-modifying requests with a
submitfunction. - State management: Automatically handles
submitting,error, andresponsestates. - Lifecycle callbacks: Provides hooks like
onSubmitSuccess,onSubmitError, andonSubmitFinishedto manage side effects cleanly. - Controlled requests: Fine-grained control over request execution and data submission.
- Deep cache integration: Leverages the central cache to keep data consistent across the application.
If you only need to fetch data, consider using the
useFetchhook.
Quick Start
To use useSubmit, provide a prepared Request instance. The hook returns a submit function to
trigger the request, along with state variables like submitting to track its progress.
Initialization
const { submit, submitting, onSubmitSuccess, onSubmitError, onSubmitFinished } = useSubmit(postLogin);
How it works?
useSubmit executes a request when a submit() function returned from it gets triggered. It uses dependency
tracking to limit re-rendering and improve performance. Under the hood, communication with the core systems is
established by event emitters. Many "helper hooks" (such as onSubmitSuccess, onSubmitError, onSubmitFinished,
etc.) are returned; these will help handle the request flow and lifecycle. This approach avoids overloading the base
hook with callback logic. It also helps improve code readability, decreases code complication, and promotes more
organized code.
import { useSubmit } from "@hyper-fetch/react";
import { postLogin } from "server";
interface Values {
email: string;
password: string;
}
const LoginPage: React.FC = () => {
const { submit, submitting, onSubmitSuccess, onSubmitError, onSubmitFinished } = useSubmit(postLogin);
onSubmitSuccess(({ response }) => {
console.log(response); // { token: string, refreshToken: string }
});
onSubmitError(({ response }) => {
console.log(response); // { message: string }
});
onSubmitFinished(({ response }) => {
const [payload, error, status] = response;
console.log(payload); // { token: string, refreshToken: string } | null
console.log(error); // { message: string } | null
console.log(status); // 200 / 400 / 404 / 500 ...
});
const onSubmit = (values: Values) => {
submit({ data: values });
};
return (
<Formik initialValues={initialValues} onSubmit={onSubmit} validationSchema={validationSchema}>
<Form>
{error && <Alert severity="error">{error.error_message}</Alert>}
<FormInput name="email" label="Email" placeholder="Fill your email" />
<FormInput type="password" name="password" label="Password" placeholder="Fill your password" />
<Button type="submit" variant="contained" disabled={submitting} className={styles.submit}>
Log in
</Button>
</Form>
</Formik>
);
};
Event Handlers
useSubmit provides several hooks to handle side effects at different stages of the request lifecycle. This approach
keeps your component logic clean by separating actions like showing notifications or redirecting from the submission
itself.
onSubmitSuccess: Fires when the request completes successfully. It receives the successresponse.onSubmitError: Fires when the request fails. It receives theerrorobject.onSubmitFinished: Fires when the request is finished, regardless of the outcome.
import { useSubmit } from "@hyper-fetch/react";
import { postLogin } from "server";
const LoginPage = () => {
const { submit, onSubmitSuccess, onSubmitError, onSubmitFinished } = useSubmit(postLogin);
onSubmitSuccess(({ response }) => {
console.log("Login successful:", response);
toast({ title: "Success", message: "Logged in!", type: "success" });
});
onSubmitError(({ error }) => {
console.error("Login failed:", error);
toast({ title: "Error", message: error.message, type: "error" });
});
onSubmitFinished((response) => {
console.log("Request finished", response);
});
const handleLogin = (values) => {
submit({ data: values });
};
// ... form rendering logic
};
Passing Data and Parameters
You can pass data, and parameters to your request in two main ways.
Statically on the Request
For data that doesn't change, you can set it directly on the Request instance.
const { submit } = useSubmit(patchUser.setParams({ userId: 1 }).setData({ name: "New Name" }));
// Later in your component
submit();
Dynamically with the submit function
For dynamic data, such as from user input, pass it in the options object to the submit function. This is the most
common approach.
const { submit } = useSubmit(patchUser);
const handleSubmit = (id: number, name: string) => {
submit({
data: { name },
params: { userId: id },
queryParams: { notify: "true" },
});
};
Clear State
The clearState method resets all hook state (data, error, status, extra, timestamps) back to initial values.
import { useSubmit } from "@hyper-fetch/react";
import { createUser } from "./api/users";
function CreateUserForm() {
const { submit, data, error, clearState } = useSubmit(createUser);
const handleReset = () => {
clearState();
};
return (
<div>
<button type="button" onClick={() => submit({ payload: { name: "John" } })}>
Create User
</button>
{data && <p>User created!</p>}
{error && <p>Error creating user</p>}
<button type="button" onClick={handleReset}>
Reset
</button>
</div>
);
}
Options
Customize the behavior of useSubmit by passing an options object as the second argument.
const { ... } = useSubmit(request, {
disabled: false,
// ... and more
});
Below is a full list of available options.
| Name | Type | Description |
|---|---|---|
| bounce | | Enable/disable debouncing for often changing keys or refreshing, to limit requests to server. |
| deepCompare | | Deep comparison function for hook to check for equality in incoming data, to limit rerenders. |
| dependencyTracking | | If
true
it will rerender only when values used by our component gets changed. Otherwise it will rerender on any change. |
| disabled | | Disable submitting |
| initialResponse | | If cache is empty we can use placeholder data. |
State and Methods
useSubmit returns an object containing the request's state, the submit method, and event handlers.
const {
submit,
submitting,
data,
error,
status,
onSubmitSuccess,
// ... and more
} = useSubmit(request);
Below is a full list of returned values.
| Name | Type | Description |
|---|---|---|
| bounce | | Data related to current state of the bounce usage |
| submitting | | Request loading state |
| clearState | | Reset all state fields to their initial values (data, error, status, extra → null, loading → false, etc.). |
| setData | | Action to set custom data. We can do it locally(inside hook state). If you need to update cache data use client.cache.update(). method. |
| setError | | Action to set custom error. We can do it locally(inside hook state). If you need to update cache data use client.cache.update() method. |
| setExtra | | Action to set custom additional data. We can do it locally(inside hook state). If you need to update cache data use client.cache.update() method. |
| setLoading | | Action to set custom loading. We can do it locally(inside hook state). If you need to update cache data use client.cache.update() method. |
| setRequestTimestamp | | Action to set custom timestamp. We can do it locally(inside hook state). If you need to update cache data use client.cache.update() method. |
| setResponseTimestamp | | Action to set custom timestamp. We can do it locally(inside hook state). If you need to update cache data use client.cache.update() method. |
| setRetries | | Action to set custom retries count. We can do it locally(inside hook state). If you need to update cache data use client.cache.update() method. |
| setStatus | | Action to set custom status. We can do it locally(inside hook state). If you need to turn on loading for all listening hooks use client.requestManager.events.emitLoading() method. |
| setSuccess | | Action to set custom success. We can do it locally(inside hook state). If you need to update cache data use client.cache.update() method. |
| abort | | Callback which allows to cancel ongoing requests from given queryKey. |
| onSubmitAbort | | Helper hook listening on aborting of requests. Includes
mutationContext
when
setOptimistic
is configured. Abort events are not triggering onError callbacks. |
| onSubmitDownloadProgress | | Helper hook listening on download progress ETA. We can later match given requests by their id's or request instance which holds all data which is being transferred. |
| onSubmitError | | Helper hook listening on error response. Includes
mutationContext
when
setOptimistic
is configured. |
| onSubmitFinished | | Helper hook listening on any response. Includes
mutationContext
when
setOptimistic
is configured. |
| onSubmitOfflineError | | Helper hook listening on request going into offline awaiting for network connection to be restored. It will not trigger onError when 'offline' mode is set on request. |
| onSubmitRequestStart | | Helper hook listening on request start. |
| onSubmitResponseStart | | Helper hook listening on response start(before we receive all data from server). |
| onSubmitSuccess | | Helper hook listening on success response. Includes
mutationContext
when
setOptimistic
is configured. |
| onSubmitUploadProgress | | Helper hook listening on upload progress ETA. We can later match given requests by their id's or request instance which holds all data which is being transferred. |
| refetch | | Refetch current request |
| submit | | Method responsible for triggering requests. It return Promise which will be resolved with the request. |
Optimistic Updates
useSubmit has first-class support for optimistic mutations. Configure optimistic behavior on the request with
setOptimistic, and the hook handles the entire lifecycle — running the callback before the request is sent, rolling
back on failure, and invalidating cache on success.
const patchUser = client
.createRequest<{ response: User; payload: Partial<User> }>()({
endpoint: "/users/:userId",
method: "PATCH",
})
.setOptimistic(({ client, payload, request }) => {
const snapshot = client.cache.get(getUsers.cacheKey);
client.cache.update(getUsers, (prev) => ({
...prev,
data: prev.data.map((u) =>
u.id === Number(request.params?.userId) ? { ...u, ...payload } : u,
),
}));
return {
context: { snapshot },
rollback: () => {
if (snapshot) client.cache.set(getUsers, snapshot);
},
invalidate: [getUsers],
};
});
The mutationContext returned from the optimistic callback is available in all submit lifecycle callbacks, fully typed:
const { submit, onSubmitSuccess, onSubmitError, onSubmitFinished, onSubmitAbort } = useSubmit(patchUser);
onSubmitSuccess(({ mutationContext }) => {
// mutationContext is typed as { snapshot: ... } | undefined
console.log("Updated! Previous snapshot:", mutationContext?.snapshot);
});
onSubmitError(({ mutationContext }) => {
// rollback() was already called automatically before this fires
alert("Update failed — changes have been reverted.");
});
onSubmitAbort(({ mutationContext }) => {
// abort is treated like an error — rollback runs automatically
console.log("Aborted, rolled back.");
});
onSubmitFinished(({ mutationContext }) => {
// Fires after success or error, with the same mutationContext
});
How it works:
- When
submit()is called, thesetOptimisticcallback runs before the network request. - The callback can update the cache optimistically and return
context,rollback, andinvalidate. - On success: cache keys from
invalidateare refreshed, thenonSubmitSuccessfires. - On error or abort:
rollback()is called automatically, then the error/abort callback fires. - With retries:
rollbackonly runs after the final failure — intermediate retries keep the optimistic state.
When bounce is enabled, the optimistic callback runs when the bounced request actually fires — not on every
intermediate submit() call. This means optimistic updates work correctly with debounce and throttle.
