UNPKG

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
/** * 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