A TypeScript library that provides persistent storage functionality for React Hook Form, allowing you to automatically save and restore form data using localStorage, sessionStorage, or custom storage implementations.
- Installation
- Quick Start
- API Reference
- Configuration Options
- Advanced Usage
- Examples
- TypeScript Support
- Contributing
- License
npm install react-hook-form-storage
yarn add react-hook-form-storage
pnpm add react-hook-form-storage
This library requires the following peer dependencies:
react >= 16.8.0
react-hook-form >= 7.0.0
Here's a basic example of how to use the library:
import { useForm } from 'react-hook-form';
import { useFormStorage } from 'react-hook-form-storage';
interface FormData {
username: string;
email: string;
age: number;
}
function MyForm() {
const form = useForm<FormData>({
defaultValues: {
username: '',
email: '',
age: 0,
},
});
const { isRestored } = useFormStorage('my-form', form, {
// Options go here
});
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<input {...form.register('username')} placeholder="Username" />
<input {...form.register('email')} placeholder="Email" />
<input {...form.register('age', { valueAsNumber: true })} type="number" placeholder="Age" />
<button type="submit">Submit</button>
{isRestored && <p>Form data restored from storage!</p>}
</form>
);
}
useFormStorage<T>(key: string, form: UseFormReturn<T>, options?: UseFormStorageOptions<T>)
The main hook that provides storage functionality for your React Hook Form.
-
key
(string
): A unique identifier for storing the form data in storage -
form
(UseFormReturn<T>
): The form instance returned byuseForm()
-
options
(UseFormStorageOptions<T>
): Configuration options (optional)
{
isRestored: boolean; // Indicates if data was restored from storage
save: () => void; // Manual save function
}
interface UseFormStorageOptions<T extends FieldValues> {
storage?: Storage; // Storage implementation (default: localStorage)
included?: Array<Path<T>>; // Fields to include in storage
excluded?: Array<Path<T>>; // Fields to exclude from storage
onRestore?: (data: Partial<T>) => void; // Callback when data is restored
onSave?: (data: Partial<T>) => void; // Callback when data is saved
debounce?: number; // Debounce delay in milliseconds
dirty?: boolean; // Mark fields as dirty when restored
touched?: boolean; // Mark fields as touched when restored
validate?: boolean; // Validate fields when restored
serializer?: Record<Path<T>, Serializer<T, Path<T>>>; // Custom serializers
autoSave?: boolean; // Enable/disable automatic saving
autoRestore?: boolean; // Enable/disable automatic restoration
}
-
Type:
Storage
-
Default:
localStorage
-
Description: The storage implementation to use. Can be
localStorage
,sessionStorage
, or a custom storage object that implements theStorage
interface.
// Use sessionStorage instead of localStorage
useFormStorage('my-form', form, {
storage: sessionStorage,
});
// Custom storage implementation
const customStorage = {
getItem: (key: string) => {
// Custom get logic
return myCustomStore.get(key);
},
setItem: (key: string, value: string) => {
// Custom set logic
myCustomStore.set(key, value);
},
removeItem: (key: string) => {
// Custom remove logic
myCustomStore.delete(key);
},
};
useFormStorage('my-form', form, {
storage: customStorage,
});
-
Type:
Array<Path<T>>
-
Description: Control which fields are stored. Use
included
to specify only certain fields, orexcluded
to omit specific fields.
// Only store username and email
useFormStorage('my-form', form, {
included: ['username', 'email'],
});
// Store all fields except password
useFormStorage('my-form', form, {
excluded: ['password'],
});
-
Type:
(data: Partial<T>) => void
- Description: Callbacks that are triggered when data is restored from storage or saved to storage.
useFormStorage('my-form', form, {
onRestore: (data) => {
console.log('Data restored:', data);
// Custom logic after restoration
},
onSave: (data) => {
console.log('Data saved:', data);
// Custom logic after saving
},
});
-
Type:
number
-
Default:
undefined
(no debouncing) - Description: Debounce delay in milliseconds for auto-saving. Useful to prevent excessive storage writes.
// Save data 500ms after the user stops typing
useFormStorage('my-form', form, {
debounce: 500,
});
-
Type:
boolean
-
Default:
false
- Description: Control form state when restoring data from storage.
useFormStorage('my-form', form, {
dirty: true, // Mark restored fields as dirty
touched: true, // Mark restored fields as touched
validate: true, // Validate restored fields
});
-
Type:
Record<Path<T>, Serializer<T, Path<T>>>
- Description: Custom serialization/deserialization for specific fields.
interface Serializer<T, K extends Path<T>> {
serialize: (value: PathValue<T, K>) => any;
deserialize: (value: any) => PathValue<T, K>;
}
// Example: Custom date serialization
useFormStorage('my-form', form, {
serializer: {
birthDate: {
serialize: (date: Date) => date.toISOString(),
deserialize: (dateString: string) => new Date(dateString),
},
},
});
-
Type:
boolean
-
Default:
true
-
Description: Enable or disable automatic saving. When disabled, use the returned
save
function for manual control.
const { save } = useFormStorage('my-form', form, {
autoSave: false,
});
// Manually save when needed
const handleSave = () => {
save();
};
-
Type:
boolean
-
Default:
true
-
Description: Enable or disable automatic restoration of form data from storage. When disabled, you can manually restore data using the
restore
function returned by the hook.
const { restore } = useFormStorage('my-form', form, {
autoRestore: false, // Disable automatic restoration
});
// Manually restore data when needed
const handleRestore = () => {
restore();
};
function ManualSaveForm() {
const form = useForm<FormData>();
const { save, isRestored } = useFormStorage('manual-form', form, {
autoSave: false, // Disable auto-save
});
const handleManualSave = () => {
save(); // Manually trigger save
};
return (
<form>
{/* Form fields */}
<button type="button" onClick={handleManualSave}>
Save Draft
</button>
</form>
);
}
interface ComplexFormData {
user: {
name: string;
preferences: string[];
};
settings: Map<string, any>;
createdAt: Date;
}
function ComplexForm() {
const form = useForm<ComplexFormData>();
useFormStorage('complex-form', form, {
serializer: {
'settings': {
serialize: (map: Map<string, any>) => Object.fromEntries(map),
deserialize: (obj: Record<string, any>) => new Map(Object.entries(obj)),
},
'createdAt': {
serialize: (date: Date) => date.toISOString(),
deserialize: (dateString: string) => new Date(dateString),
},
},
});
return (
// Form JSX
);
}
function ConditionalStorageForm() {
const [enableStorage, setEnableStorage] = useState(true);
const form = useForm<FormData>();
useFormStorage('conditional-form', form, {
autoSave: enableStorage,
onSave: (data) => {
if (enableStorage) {
console.log('Data saved:', data);
}
},
});
return (
<form>
<label>
<input
type="checkbox"
checked={enableStorage}
onChange={(e) => setEnableStorage(e.target.checked)}
/>
Enable auto-save
</label>
{/* Other form fields */}
</form>
);
}
import { useForm } from 'react-hook-form';
import { useFormStorage } from 'react-hook-form-storage';
interface ContactForm {
name: string;
email: string;
message: string;
}
export function ContactForm() {
const form = useForm<ContactForm>({
defaultValues: {
name: '',
email: '',
message: '',
},
});
const { isRestored } = useFormStorage('contact-form', form);
return (
<form onSubmit={form.handleSubmit(console.log)}>
<div>
<label>Name:</label>
<input {...form.register('name')} />
</div>
<div>
<label>Email:</label>
<input {...form.register('email')} type="email" />
</div>
<div>
<label>Message:</label>
<textarea {...form.register('message')} />
</div>
<button type="submit">Send</button>
{isRestored && (
<p style={{ color: 'green' }}>
Your previous input has been restored!
</p>
)}
</form>
);
}
interface CheckoutForm {
shippingAddress: {
street: string;
city: string;
zipCode: string;
};
billingAddress: {
street: string;
city: string;
zipCode: string;
};
paymentMethod: 'credit' | 'debit' | 'paypal';
cardNumber: string;
expiryDate: string;
}
export function CheckoutForm() {
const form = useForm<CheckoutForm>();
useFormStorage('checkout-form', form, {
excluded: ['cardNumber', 'expiryDate'], // Exclude sensitive payment info
debounce: 1000,
onSave: (data) => {
console.log('Checkout progress saved');
},
});
return (
<form>
{/* Shipping address fields */}
<fieldset>
<legend>Shipping Address</legend>
<input {...form.register('shippingAddress.street')} placeholder="Street" />
<input {...form.register('shippingAddress.city')} placeholder="City" />
<input {...form.register('shippingAddress.zipCode')} placeholder="ZIP Code" />
</fieldset>
{/* Billing address fields */}
<fieldset>
<legend>Billing Address</legend>
<input {...form.register('billingAddress.street')} placeholder="Street" />
<input {...form.register('billingAddress.city')} placeholder="City" />
<input {...form.register('billingAddress.zipCode')} placeholder="ZIP Code" />
</fieldset>
{/* Payment fields (not persisted) */}
<fieldset>
<legend>Payment Information</legend>
<select {...form.register('paymentMethod')}>
<option value="credit">Credit Card</option>
<option value="debit">Debit Card</option>
<option value="paypal">PayPal</option>
</select>
<input {...form.register('cardNumber')} placeholder="Card Number" />
<input {...form.register('expiryDate')} placeholder="MM/YY" />
</fieldset>
<button type="submit">Complete Order</button>
</form>
);
}
This library is written in TypeScript and provides full type safety:
// Types are automatically inferred from your form schema
interface UserForm {
name: string;
age: number;
preferences: string[];
}
const form = useForm<UserForm>();
// TypeScript will ensure field names are valid
useFormStorage('user-form', form, {
included: ['name', 'age'], // ✅ Valid field names
excluded: ['invalid'], // ❌ TypeScript error - 'invalid' doesn't exist
});
- Modern browsers with ES2017+ support
- localStorage/sessionStorage API support
- React 16.8+ (hooks support)
- Use
debounce
option to limit storage writes - Consider
included
/excluded
options for large forms - The library uses React's built-in optimization (useCallback, useMemo)
- Storage operations are performed asynchronously when possible
useFormStorage('my-form', form, {
onSave: (data) => {
// This callback only fires on successful saves
console.log('Data saved successfully');
},
onRestore: (data) => {
// This callback only fires on successful restoration
console.log('Data restored successfully');
},
});
Contributions are welcome!
MIT License.
For more examples and advanced usage patterns, please visit our GitHub repository.