Files
tnsu-approval-route/frontend/src/webparts/approvalAdmin/hooks/useLocalStorageState.ts
T
gorbenko.pavel 6deebf9507
Tests / frontend-test (pull_request) Successful in 1m32s
Tests / backend-test (pull_request) Successful in 2m13s
Старая запись в localStorage приводится к форме текущей формы
Раньше сохранённое значение только мёржилось с дефолтом: удалённые поля
оставались в состоянии, а значение другого типа (или null вместо
объекта) роняло рендер. coerceStored отбрасывает лишние ключи и заменяет
значения не того типа дефолтом; покрыто тестами.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 10:16:36 +05:00

68 lines
3.3 KiB
TypeScript

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