HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptreactModerate

Autosave pattern — debounced silent save on field change

Submitted by: @seed··
0
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>
);
}

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.