import * as React from 'react'; function isPlainObject(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } /** * Сохранённое значение, приведённое к форме initial. Запись могла быть сделана * старой версией формы, поэтому ей нельзя доверять слепо: * - объект мёржится с initial — добавленные с тех пор поля получают дефолт; * - ключи, которых в initial нет (поле удалили), отбрасываются; * - значение, чей тип не совпал с initial (было число — стала строка, было * [] — стал объект), заменяется дефолтом: иначе .map/.trim по нему уронил бы рендер; * - null/undefined в initial типа не задают — там принимаем любое сохранённое. */ export function coerceStored(initial: T, stored: unknown): T { if (initial === null || initial === undefined) return stored as T; if (Array.isArray(initial)) return (Array.isArray(stored) ? stored : initial) as unknown as T; if (isPlainObject(initial)) { if (!isPlainObject(stored)) return initial; const result: Record = { ...initial }; Object.keys(initial).forEach(k => { if (k in stored) result[k] = coerceStored(initial[k], stored[k]); }); return result as T; } return (typeof stored === typeof initial ? stored : initial) as T; } /** * Значение из localStorage под key, приведённое к форме initial (см. coerceStored), * или initial, если записи нет / она битая / localStorage недоступен. */ export function readLocalStorage(key: string, initial: T): T { try { const raw = window.localStorage.getItem(key); if (!raw) return initial; return coerceStored(initial, JSON.parse(raw)); } catch { return initial; } } /** * Как React.useState, но значение переживает перезагрузку страницы — хранится * в localStorage под key. Падение доступа к localStorage (приватный режим, * квота) не ломает форму — просто не персистит. * * Сохранённое значение приводится к форме initial (см. coerceStored): новое поле * получает дефолт, удалённое отбрасывается, значение не того типа заменяется * дефолтом — иначе старая запись в localStorage уронила бы рендер на первом же * .map/.trim по этому полю. */ export function useLocalStorageState(key: string, initial: T): [T, React.Dispatch>] { const [value, setValue] = React.useState(() => readLocalStorage(key, initial)); React.useEffect(() => { try { window.localStorage.setItem(key, JSON.stringify(value)); } catch { // приватный режим / переполненная квота — не критично, просто не персистим } }, [key, value]); return [value, setValue]; }