patterntypescriptreactModerate
Autosave pattern — debounced silent save on field change
Viewed 0 times
autosavedebounced savewatch subscriptionbeforeunloadsilent saveform persistencenote editor
Problem
Documents, notes, and profile forms that require a manual save button risk data loss if the user forgets to save or navigates away. Autosave eliminates this friction but naive implementations save on every keystroke, creating excessive server load.
Solution
Watch form values, debounce changes, and save silently with status feedback:
import { useEffect, useCallback, useRef } from 'react';
import { useForm } from 'react-hook-form';
type NoteForm = { title: string; content: string };
function NoteEditor({ initialNote }: { initialNote: NoteForm }) {
const saveStatus = useRef<'saved' | 'saving' | 'unsaved'>('saved');
const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const { register, watch, getValues } = useForm<NoteForm>({
defaultValues: initialNote,
});
const saveToServer = useCallback(async (data: NoteForm) => {
saveStatus.current = 'saving';
try {
await fetch('/api/notes', {
method: 'PUT',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
});
saveStatus.current = 'saved';
} catch {
saveStatus.current = 'unsaved';
}
}, []);
// Watch all fields; debounce saves by 1.5 seconds
useEffect(() => {
const subscription = watch(() => {
if (saveTimer.current) clearTimeout(saveTimer.current);
saveStatus.current = 'unsaved';
saveTimer.current = setTimeout(() => {
saveToServer(getValues());
}, 1500);
});
return () => subscription.unsubscribe();
}, [watch, getValues, saveToServer]);
// Save immediately on page unload
useEffect(() => {
const handler = () => saveToServer(getValues());
window.addEventListener('beforeunload', handler);
return () => window.removeEventListener('beforeunload', handler);
}, [getValues, saveToServer]);
return (
<form>
<input {...register('title')} />
<textarea {...register('content')} />
</form>
);
}
import { useEffect, useCallback, useRef } from 'react';
import { useForm } from 'react-hook-form';
type NoteForm = { title: string; content: string };
function NoteEditor({ initialNote }: { initialNote: NoteForm }) {
const saveStatus = useRef<'saved' | 'saving' | 'unsaved'>('saved');
const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const { register, watch, getValues } = useForm<NoteForm>({
defaultValues: initialNote,
});
const saveToServer = useCallback(async (data: NoteForm) => {
saveStatus.current = 'saving';
try {
await fetch('/api/notes', {
method: 'PUT',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
});
saveStatus.current = 'saved';
} catch {
saveStatus.current = 'unsaved';
}
}, []);
// Watch all fields; debounce saves by 1.5 seconds
useEffect(() => {
const subscription = watch(() => {
if (saveTimer.current) clearTimeout(saveTimer.current);
saveStatus.current = 'unsaved';
saveTimer.current = setTimeout(() => {
saveToServer(getValues());
}, 1500);
});
return () => subscription.unsubscribe();
}, [watch, getValues, saveToServer]);
// Save immediately on page unload
useEffect(() => {
const handler = () => saveToServer(getValues());
window.addEventListener('beforeunload', handler);
return () => window.removeEventListener('beforeunload', handler);
}, [getValues, saveToServer]);
return (
<form>
<input {...register('title')} />
<textarea {...register('content')} />
</form>
);
}
Why
watch() from React Hook Form returns an observable subscription. Debouncing inside the subscription handler ensures that saves fire only after the user pauses, not on every keystroke. The beforeunload handler catches the case where the user closes the tab mid-edit.
Gotchas
- Always clear the debounce timer in watch's cleanup — the subscription unsubscribe does not cancel pending timeouts
- beforeunload cannot show a custom message in modern browsers — only the browser's generic 'Changes may not be saved' prompt
- If the user edits faster than the debounce period, only the last version is saved — this is intentional
- Show a subtle status indicator ('Saving...', 'Saved', 'Unsaved changes') so users know the save is happening
Revisions (0)
No revisions yet.