Auto save with a custom hook in Next.js 13

Auto save with a custom hook is about twenty lines: this one submits a form on a timer, so a long post saves itself while you write. I will show the whole thing, and then the three changes I make before it goes into production — my own editor has since moved on to a different approach, and I will explain why.
The hook does not know whether anything changed. It clicks submit on a schedule, dirty or not. That is the weakness to design around before you ship it.
Import useEffect from "react":
import { useEffect } from "react";
useEffect is the hook for side effects in a functional component. A timer is a side effect, so this is the right home for it.
Auto save with a custom hook: create useFormSubmitInterval
function useFormSubmitInterval(intervalMs: number): void {
// ...
}
One argument, the interval in milliseconds. It returns nothing — it exists purely for the effect it sets up.
Inside the useEffect hook:
useEffect(() => {
// ...
}, [intervalMs]);
The main logic lives here. It runs when the component mounts, and again if intervalMs changes.
Set up an interval to submit the form:
const formSubmitInterval = setInterval(() => {
const submitButton = document.querySelector('button[type="submit"]');
setInterval runs the function repeatedly. Inside it, we look for a submit button in the page.
Check if a submit button was found:
if (submitButton) {
// Simulate a click event on the submit button
(submitButton as HTMLButtonElement).click();
}
If it is there, we click it — the same as the user pressing Save. Note the guard: without it, a page without that button throws on every tick.
Cleanup the interval on unmount:
return () => {
clearInterval(formSubmitInterval);
};
Do not skip this. Without the cleanup the timer survives the component and keeps firing at a page that has gone, which is a memory leak and, worse, a save against the wrong record.
Dependency array [intervalMs]:
The effect re-runs whenever intervalMs changes, so the old timer is cleared and a new one started at the new duration.
Here is complete custom hook:
import { useEffect } from "react";
function useFormSubmitInterval(intervalMs: number): void {
useEffect(() => {
const formSubmitInterval = setInterval(() => {
const submitButton = document.querySelector('button[type="submit"]');
if (submitButton) {
// Simulate a click event on the submit button
(submitButton as HTMLButtonElement).click();
}
// You can add a condition here to handle the case when submitButton is null.
}, intervalMs);
return () => {
clearInterval(formSubmitInterval);
};
}, [intervalMs]);
}
export default useFormSubmitInterval;
Call the hook where you have form with submit and want to use this custom hook
// = = = = = = = = = = = =
// AUTO SAVE DRAFT
// = = = = = = = = = = = =
const intervalMs = 5 * 60 * 1000; // 2 minutes
useFormSubmitInterval(intervalMs);
What I would change before using this in production
Three things, in the order they will bite you:
- Correct the comment. 5 * 60 * 1000 is five minutes, not two. Small thing, but it is the kind of mismatch someone trusts later.
- It saves whether or not you changed anything. Every tick writes a record and bumps the updated date. Gate it on the form being dirty.
- querySelector is fragile. It grabs the first submit button in the whole document — not necessarily yours. Calling the form's own submit handler is steadier than pretending to be a user.
My own editor now does it differently. It snapshots the draft to the browser on a timer and offers it back if the tab dies, and the real save stays a deliberate press. That distinction matters more than the timer does.
Questions
Questions this post answers
How does the auto save custom hook know when to submit?
- It does not know anything about your form. It runs a timer with setInterval, looks for a submit button in the page, and clicks it — the same as the user pressing Save. The guard around that lookup matters: without it, a page without that button throws on every tick.
Does the hook save only when something has changed?
- No, and this is its main weakness. It clicks submit on a schedule, dirty or not, so every tick writes a record and bumps the updated date. Gate it on the form being dirty before you ship it.
Why does the interval need a cleanup?
- Without the cleanup the timer survives the component and keeps firing at a page that has gone. That is a memory leak and, worse, a save against the wrong record.
Would you use this approach in production today?
- My own editor no longer does it this way. It snapshots the draft to the browser on a timer and offers it back if the tab dies, and the real save stays a deliberate press. That distinction matters more than the timer does.