use-form-auto-save
Version:
A customizable React hook for automatically saving and restoring form data with support for localStorage, sessionStorage, and external APIs.
189 lines • 7.66 kB
JavaScript
/**
* This file contains the `useFormAutoSave` hook, a custom React hook for automatically saving form data.
* It supports localStorage, sessionStorage, and API-based saving with features like debouncing, retries, and error handling.
*
* Related utilities:
* - `useSaveHandler`: Handles the save logic.
* - `useRetryHandler`: Manages retry logic for failed saves.
*/
import { useEffect, useState, useCallback, useRef } from 'react';
import { useWatch } from 'react-hook-form';
import isEqual from 'lodash.isequal';
import { useSaveHandler } from './useSaveHandler';
import { useRetryHandler } from './useRetryHandler';
import { validateConfig } from './validateConfig';
/**
* useFormAutoSave - Custom React Hook for automatically saving form data.
*
* This hook provides an efficient and customizable solution to automatically save form data
* to either localStorage, sessionStorage, or an external API, based on your preference.
* It supports debouncing, error handling, automatic retry mechanisms, and form restoration.
*
* @param {AutoSaveConfig} config - Configuration object to customize hook behavior.
*
* @property {string} config.formKey - A unique key identifying the form data for storage/retrieval.
* @property {object} [config.formData] - Form data object (use when manually handling form state).
* @property {Control<any>} [config.control] - React Hook Form control object (use with React Hook Form).
* @property {number} [config.debounceTime=1000] - Delay in milliseconds before auto-save triggers after changes.
* @property {"localStorage"|"sessionStorage"|"api"} [config.storageType="localStorage"] - Storage medium.
* @property {SaveFunction} [config.saveFunction] - Custom asynchronous function for API-based saving.
* @property {ErrorCallback} [config.onError] - Callback triggered on save error.
* @property {number} [config.maxRetries=3] - Number of retry attempts on failure before pausing auto-save.
* @property {boolean} [config.skipInitialSave=false] - Skip auto-saving on initial render.
* @property {boolean} [config.debug=false] - Enable debug logging for the hook.
*
* @returns {object} Object containing methods and state for managing auto-save functionality:
* - restoreFormData(): Function to retrieve saved form data (null if using API storage).
* - resumeAutoSave(): Function to manually resume auto-save if paused after retries.
* - isSaving: Indicates if auto-save is currently in progress.
* - isSaveSuccessful: Indicates if the last auto-save operation was successful.
* - isAutoSavePaused: Indicates if auto-save is paused due to consecutive errors.
* - setLastSavedData: Function to manually update the internally tracked last saved data. Useful for syncing the last saved state manually.
*
* @example
* // Basic usage with localStorage
* const { restoreFormData, isSaving } = useFormAutoSave({
* formKey: 'userForm',
* formData: formState,
* });
*
* @example
* // Advanced usage with API storage and error handling
* const { isSaveSuccessful, resumeAutoSave } = useFormAutoSave({
* formKey: 'userProfile',
* control,
* storageType: 'api',
* debounceTime: 2000,
* saveFunction: async (data) => await apiClient.saveUserProfile(data),
* onError: (err) => toast.error("Auto-save failed."),
* maxRetries: 5,
* });
*
* @note
* - When using 'api' storage type, you must provide a `saveFunction`.
* - `restoreFormData` is not available when using 'api' storage type.
* - The hook automatically pauses auto-save after exhausting retries; use `resumeAutoSave` to restart.
*
* - Auto-save is skipped under the following conditions:
* - watchedFormState or formKey is missing.
* - Auto-save is paused due to consecutive errors.
* - The form state is empty.
* - The initial save is skipped (skipInitialSave is true).
* - The form data has not changed since the last save.
*
* @author Damyant Jain (https://github.com/damyantjain)
* @license MIT
*/
export const useFormAutoSave = (config) => {
const { formKey, debounceTime = 1000, storageType = 'localStorage', saveFunction, onError, maxRetries = 3, skipInitialSave = false, debug = false, } = config;
validateConfig(config);
const watchedFormState = config.control
? useWatch({ control: config.control })
: config.formData;
const logDebug = useCallback((...args) => {
if (debug) {
console.log('[useFormAutoSave]', ...args);
}
}, [debug]);
const [lastSavedData, setLastSavedData] = useState(null);
const [isSaving, setIsSaving] = useState(false);
const [isSaveSuccessful, setIsSaveSuccessful] = useState(false);
const [retryCount, setRetryCount] = useState(0);
const [isAutoSavePaused, setIsAutoSavePaused] = useState(false);
const [shouldRetry, setShouldRetry] = useState(false);
const hasMounted = useRef(false);
const resumeAutoSave = useCallback(() => {
logDebug('Manually resuming auto-save.');
setRetryCount(0);
setIsAutoSavePaused(false);
setShouldRetry(false);
}, [logDebug]);
const shouldSkipAutoSave = () => {
if (!watchedFormState || !formKey || isAutoSavePaused) {
logDebug('Auto-save skipped: missing form state/key or paused.');
return true;
}
if (Object.keys(watchedFormState).length === 0) {
logDebug('Auto-save skipped: empty form state.');
return true;
}
if (!hasMounted.current) {
hasMounted.current = true;
if (skipInitialSave) {
logDebug('Auto-save skipped: initial save skipped.');
return true;
}
}
if (lastSavedData && isEqual(lastSavedData, watchedFormState)) {
logDebug('Auto-save skipped: data unchanged.');
return true;
}
return false;
};
const { performSave } = useSaveHandler({
formKey,
storageType,
saveFunction,
onError,
maxRetries,
logDebug,
retryCount,
setRetryCount,
setShouldRetry,
setIsAutoSavePaused,
setIsSaving,
setIsSaveSuccessful,
setLastSavedData,
});
useEffect(() => {
logDebug('Effect triggered.');
if (shouldSkipAutoSave())
return;
const handler = setTimeout(() => {
performSave(watchedFormState);
}, debounceTime);
return () => clearTimeout(handler);
}, [
watchedFormState,
formKey,
debounceTime,
storageType,
saveFunction,
onError,
retryCount,
isAutoSavePaused,
skipInitialSave,
lastSavedData,
logDebug,
performSave,
]);
useRetryHandler({
shouldRetry,
retryCount,
maxRetries,
logDebug,
onRetry: () => {
setShouldRetry(false);
setRetryCount((prev) => prev + 1);
},
});
const restoreFormData = () => {
if (storageType === 'api') {
logDebug('Restore functionality is unavailable for API storage.');
return null;
}
const storage = storageType === 'localStorage' ? localStorage : sessionStorage;
const savedData = storage.getItem(formKey);
logDebug('Restoring form data:', savedData);
return savedData ? JSON.parse(savedData) : null;
};
return {
restoreFormData,
isSaving,
isSaveSuccessful,
isAutoSavePaused,
resumeAutoSave,
setLastSavedData,
};
};
//# sourceMappingURL=useFormAutoSave.js.map