Skip to main content

Autosave Multi-Step Forms

Multi-step forms are common in applications, and preserving user progress across steps is crucial. This guide shows you how to implement autosave for multi-step forms.

Basic Multi-Step Form

import { useState } from 'react';
import { useFormAutosave } from '@form-guardian/react';

interface FormData {
step1: { name: string; email: string };
step2: { address: string; city: string };
step3: { payment: string; terms: boolean };
}

function MultiStepForm() {
const [step, setStep] = useState(1);

const { formRef, clearDraft } = useFormAutosave('multi-step-form', {
autoRestore: true,
ttl: { days: 7 }, // Keep draft for 7 days
});

const handleNext = () => {
if (step < 3) {
setStep(step + 1);
}
};

const handlePrev = () => {
if (step > 1) {
setStep(step - 1);
}
};

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Submit all steps
await clearDraft();
// Navigate to success page
};

return (
<form ref={formRef} onSubmit={handleSubmit}>
{step === 1 && (
<div>
<h2>Step 1: Personal Information</h2>
<input name="step1.name" placeholder="Name" />
<input name="step1.email" type="email" placeholder="Email" />
<button type="button" onClick={handleNext}>Next</button>
</div>
)}

{step === 2 && (
<div>
<h2>Step 2: Address</h2>
<input name="step2.address" placeholder="Address" />
<input name="step2.city" placeholder="City" />
<button type="button" onClick={handlePrev}>Previous</button>
<button type="button" onClick={handleNext}>Next</button>
</div>
)}

{step === 3 && (
<div>
<h2>Step 3: Payment</h2>
<input name="step3.payment" placeholder="Payment method" />
<label>
<input name="step3.terms" type="checkbox" />
Accept terms
</label>
<button type="button" onClick={handlePrev}>Previous</button>
<button type="submit">Submit</button>
</div>
)}
</form>
);
}

With React Hook Form

For more complex validation:

import { useForm } from 'react-hook-form';
import { useFormAutosave } from '@form-guardian/react';
import { useState, useEffect } from 'react';

function MultiStepFormWithRHF() {
const [step, setStep] = useState(1);

const { register, handleSubmit, setValue, getValues, trigger } = useForm({
defaultValues: {
name: '',
email: '',
address: '',
city: '',
payment: '',
terms: false,
},
});

const { formRef, restoreValues, clearDraft } = useFormAutosave(
'multi-step-form',
{ autoRestore: false }
);

// Restore draft on mount
useEffect(() => {
restoreValues(
(field, value) => setValue(field, value, { shouldValidate: false }),
() => getValues()
);
}, []);

const handleNext = async () => {
// Validate current step before proceeding
const fields = step === 1
? ['name', 'email']
: step === 2
? ['address', 'city']
: ['payment', 'terms'];

const isValid = await trigger(fields);
if (isValid && step < 3) {
setStep(step + 1);
}
};

const handlePrev = () => {
if (step > 1) {
setStep(step - 1);
}
};

const onSubmit = async (data: any) => {
await submitForm(data);
await clearDraft();
};

return (
<form ref={formRef} onSubmit={handleSubmit(onSubmit)}>
{step === 1 && (
<div>
<h2>Step 1: Personal Information</h2>
<input {...register('name', { required: true })} />
<input {...register('email', { required: true })} type="email" />
<button type="button" onClick={handleNext}>Next</button>
</div>
)}

{step === 2 && (
<div>
<h2>Step 2: Address</h2>
<input {...register('address', { required: true })} />
<input {...register('city', { required: true })} />
<button type="button" onClick={handlePrev}>Previous</button>
<button type="button" onClick={handleNext}>Next</button>
</div>
)}

{step === 3 && (
<div>
<h2>Step 3: Payment</h2>
<input {...register('payment', { required: true })} />
<label>
<input {...register('terms', { required: true })} type="checkbox" />
Accept terms
</label>
<button type="button" onClick={handlePrev}>Previous</button>
<button type="submit">Submit</button>
</div>
)}
</form>
);
}

Show Progress Indicator

function MultiStepFormWithProgress() {
const [step, setStep] = useState(1);
const totalSteps = 3;

const { formRef, hasDraft, clearDraft } = useFormAutosave('multi-step-form', {
autoRestore: true,
});

return (
<div>
{/* Progress indicator */}
<div className="progress-bar">
{Array.from({ length: totalSteps }).map((_, i) => (
<div
key={i}
className={i + 1 <= step ? 'completed' : 'pending'}
>
Step {i + 1}
</div>
))}
</div>

{/* Draft indicator */}
{hasDraft && (
<div className="draft-notice">
You have a saved draft. Your progress will be restored.
</div>
)}

<form ref={formRef}>
{/* Form steps */}
</form>
</div>
);
}

Per-Step Autosave

If you want separate drafts for each step:

function MultiStepFormPerStep() {
const [step, setStep] = useState(1);

// Separate autosave for each step
const step1 = useFormAutosave('multi-step-form-step1', { autoRestore: true });
const step2 = useFormAutosave('multi-step-form-step2', { autoRestore: true });
const step3 = useFormAutosave('multi-step-form-step3', { autoRestore: true });

const currentAutosave = step === 1 ? step1 : step === 2 ? step2 : step3;

const handleSubmit = async () => {
// Clear all step drafts
await step1.clearDraft();
await step2.clearDraft();
await step3.clearDraft();
// Submit
};

return (
<form ref={currentAutosave.formRef}>
{/* Form content */}
</form>
);
}

Best Practices

  1. Use appropriate TTL - Multi-step forms often take longer, use longer TTL
  2. Show progress - Let users know which step they're on
  3. Validate before proceeding - Don't let users skip invalid steps
  4. Clear all drafts on completion - Clean up after successful submission
  5. Handle navigation - Save draft when user navigates away

Common Pitfalls

❌ Don't Forget to Clear Drafts

// ❌ Wrong - draft persists after submission
const onSubmit = async () => {
await submitForm();
// Forgot to clear draft!
};

✅ Clear Drafts After Submission

// ✅ Correct - clear draft after successful submission
const onSubmit = async () => {
await submitForm();
await clearDraft(); // Clear draft
};

Next Steps