Form Persistence
Auto-save form data to localStorage with restore dialog
Overview
Forms can automatically save their state to localStorage and restore it when the user returns. This prevents data loss on accidental page refreshes.
Full example
The complete sandbox example, read directly from the form-develop-app source at build time.
'use client'
import { Box, Heading, Text, VStack } from '@chakra-ui/react'
import { Form } from '@letar/forms'
import { useState } from 'react'
import { z } from 'zod/v4'
import { DemoPageLayout, SubmittedDataPreview } from '../_components'
/**
* Simple schema for persistence demo
*/
const DemoSchema = z.object({
title: z
.string()
.min(2)
.meta({
ui: { title: 'Title', placeholder: 'Enter a title' },
}),
description: z.string().meta({
ui: { title: 'Description', placeholder: 'Enter description...' },
}),
priority: z.string().meta({
ui: { title: 'Priority' },
}),
category: z.string().meta({
ui: { title: 'Category', placeholder: 'Select category' },
}),
})
type DemoFormData = z.infer<typeof DemoSchema>
const priorityOptions = [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium' },
{ label: 'High', value: 'high' },
]
const categoryOptions = [
{ label: 'Work', value: 'work' },
{ label: 'Personal', value: 'personal' },
{ label: 'Shopping', value: 'shopping' },
{ label: 'Health', value: 'health' },
]
const initialValues: DemoFormData = {
title: '',
description: '',
priority: '',
category: '',
}
export default function PersistenceDemoPage() {
const [submittedData, setSubmittedData] = useState<DemoFormData | null>(null)
const handleSubmit = (data: DemoFormData) => {
setSubmittedData(data)
}
return (
<DemoPageLayout
title="Persistence Demo"
description="This form automatically saves your progress to localStorage. Try filling in some fields, then refresh the page - you will be asked if you want to restore your data."
>
<Form
initialValue={initialValues}
schema={DemoSchema}
onSubmit={handleSubmit}
persistence={{
key: 'persistence-demo-form',
debounceMs: 500,
dialogTitle: 'Restore saved data?',
dialogDescription: 'You have unsaved changes from a previous session. Would you like to restore them?',
restoreButtonText: 'Restore',
discardButtonText: 'Start fresh',
}}
>
<VStack gap={4} align="stretch">
<Form.Field.String name="title" />
<Form.Field.Textarea name="description" rows={3} />
<Form.Field.RadioGroup name="priority" options={priorityOptions} orientation="horizontal" />
<Form.Field.Select name="category" options={categoryOptions} />
<Form.Button.Submit>Submit</Form.Button.Submit>
</VStack>
</Form>
<SubmittedDataPreview
data={submittedData}
title="Form submitted successfully! localStorage data has been cleared."
/>
<Box
p={4}
bg="blue.50"
borderRadius="md"
borderWidth="1px"
borderColor="blue.200"
_dark={{ bg: 'blue.900/20', borderColor: 'blue.700' }}
>
<Heading size="sm" mb={2} color="blue.700" _dark={{ color: 'blue.300' }}>
How it works:
</Heading>
<VStack align="stretch" gap={2} fontSize="sm">
<Text>1. Fill in some fields in the form above</Text>
<Text>2. Refresh the page (F5 or Ctrl+R)</Text>
<Text>3. A dialog will appear asking to restore your data</Text>
<Text>4. Click "Restore" to continue where you left off</Text>
<Text>5. Click "Start fresh" to discard saved data</Text>
<Text>6. On successful submit, saved data is automatically cleared</Text>
</VStack>
</Box>
</DemoPageLayout>
)
}shadcn-вариант этого примера ещё не готов.
Vue-пример для этого поля появится позже.
Angular-пример для этого поля появится позже.
Basic Usage
<Form
schema={Schema}
initialValue={defaults}
onSubmit={handleSubmit}
persistence={{
key: 'checkout-form',
debounceMs: 500,
}}
>
<Form.Field.String name="name" />
<Form.Field.String name="email" />
<Form.Button.Submit>Submit</Form.Button.Submit>
</Form>How It Works
- User types → form state is debounced and saved to
localStorage - Page refreshes → a restore dialog appears: "You have unsaved data. Restore?"
- User clicks Restore → form is populated with saved values
- User submits → localStorage entry is automatically cleared
Configuration
persistence={{
key: 'my-form', // localStorage key (required)
debounceMs: 500, // save debounce in ms (default: 500)
dialogTitle: 'Restore draft?',
dialogDescription: 'You have unsaved changes from your last session.',
restoreButtonText: 'Restore',
discardButtonText: 'Discard',
excludeFields: ['password', 'cvv'], // never written to the snapshot — see below
}}Excluding sensitive fields
Password, card number, CVV, expiry date and other auth/payment data must never be written to
localStorage, even when the rest of the form is persisted. excludeFields strips those keys from
the object before serialization (shallow omit only). On restore the excluded fields are simply
absent from savedData — the form doesn't overwrite them and keeps whatever is in initialValue.
<Form
schema={PaymentSchema}
initialValue={defaults}
onSubmit={handleSubmit}
persistence={{
key: 'payment-form',
excludeFields: ['cardNumber', 'cvv', 'expiryDate'],
}}
>
...
</Form>The same option is available on the low-level useFormPersistence hook directly, for forms that
assemble persistence manually instead of using <Form>.
Custom Persistence
For advanced cases (e.g., IndexedDB, server-side), use useOfflineForm from the Offline module instead.