Skip to main content

Autosave + PWA Offline Mode

Form Guardian works perfectly with Progressive Web Apps (PWA) for offline form functionality. This guide shows you how to implement it.

Basic PWA Setup

Form Guardian automatically works offline since it uses IndexedDB:

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

function PWAForm() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
const { formRef, hasDraft, clearDraft } = useFormAutosave('pwa-form', {
autoRestore: true,
ttl: { days: 30 }, // Longer TTL for offline scenarios
});

useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);

window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);

return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

if (isOnline) {
// Submit immediately
await submitForm();
await clearDraft();
} else {
// Queue for later submission
await queueForSubmission();
// Keep draft until online
}
};

return (
<form ref={formRef} onSubmit={handleSubmit}>
<div className={isOnline ? 'online' : 'offline'}>
{isOnline ? '🟢 Online' : '🔴 Offline'}
</div>
<input name="title" placeholder="Title" />
<textarea name="content" placeholder="Content" />
<button type="submit">
{isOnline ? 'Submit' : 'Save for Later'}
</button>
</form>
);
}

Queue Submissions When Offline

Queue form submissions when offline and submit when back online:

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

interface QueuedSubmission {
id: string;
data: any;
timestamp: number;
}

function PWAFormWithQueue() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
const [queuedSubmissions, setQueuedSubmissions] = useState<QueuedSubmission[]>([]);

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

// Load queued submissions from IndexedDB
useEffect(() => {
loadQueuedSubmissions().then(setQueuedSubmissions);
}, []);

// Submit queued submissions when back online
useEffect(() => {
if (isOnline && queuedSubmissions.length > 0) {
submitQueuedSubmissions();
}
}, [isOnline]);

const queueForSubmission = async (data: any) => {
const submission: QueuedSubmission = {
id: Date.now().toString(),
data,
timestamp: Date.now(),
};

// Save to IndexedDB queue
await saveToQueue(submission);
setQueuedSubmissions([...queuedSubmissions, submission]);
};

const submitQueuedSubmissions = async () => {
for (const submission of queuedSubmissions) {
try {
await submitForm(submission.data);
await removeFromQueue(submission.id);
} catch (error) {
console.error('Failed to submit queued form:', error);
}
}
setQueuedSubmissions([]);
};

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const data = Object.fromEntries(formData);

if (isOnline) {
await submitForm(data);
await clearDraft();
} else {
await queueForSubmission(data);
// Keep draft - user might want to edit
}
};

return (
<form ref={formRef} onSubmit={handleSubmit}>
<div>
{isOnline ? '🟢 Online' : '🔴 Offline'}
{queuedSubmissions.length > 0 && (
<span> ({queuedSubmissions.length} queued)</span>
)}
</div>
<input name="title" placeholder="Title" />
<textarea name="content" placeholder="Content" />
<button type="submit">
{isOnline ? 'Submit' : 'Save for Later'}
</button>
</form>
);
}

Service Worker Integration

Integrate with Service Worker for better offline support:

// service-worker.js
self.addEventListener('sync', (event) => {
if (event.tag === 'submit-forms') {
event.waitUntil(submitQueuedForms());
}
});

async function submitQueuedForms() {
const queue = await getQueuedSubmissions();
for (const submission of queue) {
try {
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(submission.data),
});
await removeFromQueue(submission.id);
} catch (error) {
console.error('Background sync failed:', error);
}
}
}
// In your React component
function PWAFormWithServiceWorker() {
const { formRef, clearDraft } = useFormAutosave('pwa-form', {
autoRestore: true,
});

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const data = Object.fromEntries(formData);

if (navigator.onLine) {
await submitForm(data);
await clearDraft();
} else {
// Queue for background sync
await queueForBackgroundSync(data);
// Keep draft
}
};

return (
<form ref={formRef} onSubmit={handleSubmit}>
<input name="title" placeholder="Title" />
<button type="submit">Submit</button>
</form>
);
}

Show Offline Indicator

Provide clear feedback about offline status:

function PWAFormWithIndicator() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
const { formRef, hasDraft } = useFormAutosave('pwa-form', {
autoRestore: true,
});

useEffect(() => {
const updateOnlineStatus = () => setIsOnline(navigator.onLine);
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
return () => {
window.removeEventListener('online', updateOnlineStatus);
window.removeEventListener('offline', updateOnlineStatus);
};
}, []);

return (
<div>
<div className={`status-bar ${isOnline ? 'online' : 'offline'}`}>
{isOnline ? (
<span>🟢 Connected</span>
) : (
<span>🔴 Offline - Your changes are saved locally</span>
)}
{hasDraft && <span> • Draft saved</span>}
</div>
<form ref={formRef}>
<input name="title" placeholder="Title" />
<textarea name="content" placeholder="Content" />
<button type="submit">
{isOnline ? 'Submit' : 'Save for Later'}
</button>
</form>
</div>
);
}

Sync When Back Online

Automatically sync when connection is restored:

function PWAFormWithAutoSync() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
const { formRef, hasDraft, clearDraft } = useFormAutosave('pwa-form', {
autoRestore: true,
});

useEffect(() => {
const handleOnline = async () => {
setIsOnline(true);
// Auto-submit if there's a draft
if (hasDraft) {
const formData = getFormData();
try {
await submitForm(formData);
await clearDraft();
showNotification('Form submitted successfully!');
} catch (error) {
showNotification('Failed to submit. Please try again.');
}
}
};

window.addEventListener('online', handleOnline);
return () => window.removeEventListener('online', handleOnline);
}, [hasDraft]);

return (
<form ref={formRef}>
<input name="title" placeholder="Title" />
<button type="submit">Submit</button>
</form>
);
}

Best Practices

  1. Longer TTL for offline - Users might be offline for days
  2. Show clear offline indicators - Let users know their data is saved
  3. Queue submissions - Don't lose data when offline
  4. Sync when online - Automatically submit queued forms
  5. Handle sync errors - Retry failed submissions

Common Pitfalls

❌ Don't Clear Drafts When Offline

// ❌ Wrong - user loses data if offline
const handleSubmit = async () => {
await submitForm(); // Fails when offline
await clearDraft(); // Draft cleared anyway
};

✅ Keep Drafts When Offline

// ✅ Correct - keep draft until successfully submitted
const handleSubmit = async () => {
if (isOnline) {
await submitForm();
await clearDraft(); // Only clear after success
} else {
await queueForSubmission(); // Queue for later
// Keep draft
}
};

Next Steps