+12








fcd7c979df
Co-authored-by: Artur <145776561+arturict@users.noreply.github.com> Co-authored-by: arturict <arturict@users.noreply.github.com> Co-authored-by: Thomas <thomas@ajnart.fr> Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com> Co-authored-by: Danura30082 <danura30082+github@gmail.com> Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com> Co-authored-by: ajnart <thomas@ajnart.fr> Co-authored-by: ajnart <thomas@ajnart.dev> Co-authored-by: chenjun <chenjun@foxmail.com> Co-authored-by: Wikibear <wikibear@users.noreply.github.com> Co-authored-by: Bergasha <bergasha@gmail.com> Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com> Co-authored-by: Lucas Araujo <lucas.lra@gmail.com> Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com> Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social> Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com> Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com> Co-authored-by: thrawnx <tight.bulb9384@fastmail.com> Co-authored-by: Chris <58918656+nordada@users.noreply.github.com> Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com> Co-authored-by: Yanis <naamaneyanis@gmail.com> Co-authored-by: Wikibear <me@sej.de> Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
147 lines
4.6 KiB
TypeScript
147 lines
4.6 KiB
TypeScript
"use client";
|
|
|
|
import type { Dispatch, SetStateAction } from "react";
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
|
interface StoredWidgetState {
|
|
version: number;
|
|
value: unknown;
|
|
}
|
|
|
|
interface UseLocalWidgetStateOptions<T> {
|
|
key: string | undefined;
|
|
version: number;
|
|
defaultValue: T | (() => T);
|
|
validate?: (value: unknown) => value is T;
|
|
migrate?: (value: unknown, storedVersion: number) => T | undefined;
|
|
}
|
|
|
|
const resolveDefaultValue = <T>(value: T | (() => T)): T =>
|
|
typeof value === "function" ? (value as () => T)() : value;
|
|
|
|
export const serializeLocalWidgetState = <T>(version: number, value: T): string => JSON.stringify({ version, value });
|
|
|
|
export const parseLocalWidgetState = <T>(
|
|
serialized: string,
|
|
version: number,
|
|
validate?: (value: unknown) => value is T,
|
|
migrate?: (value: unknown, storedVersion: number) => T | undefined,
|
|
): T | undefined => {
|
|
try {
|
|
const parsed: unknown = JSON.parse(serialized);
|
|
if (typeof parsed !== "object" || parsed === null || !("version" in parsed) || !("value" in parsed)) {
|
|
return undefined;
|
|
}
|
|
|
|
const stored = parsed as StoredWidgetState;
|
|
if (typeof stored.version !== "number") return undefined;
|
|
if (stored.version !== version) {
|
|
const migrated = migrate?.(stored.value, stored.version);
|
|
if (migrated === undefined || (validate && !validate(migrated))) return undefined;
|
|
return migrated;
|
|
}
|
|
if (validate && !validate(stored.value)) return undefined;
|
|
return stored.value as T;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
export const useLocalWidgetState = <T>({
|
|
key,
|
|
version,
|
|
defaultValue,
|
|
validate,
|
|
migrate,
|
|
}: UseLocalWidgetStateOptions<T>): [T, Dispatch<SetStateAction<T>>, () => void] => {
|
|
const defaultValueRef = useRef(defaultValue);
|
|
const validateRef = useRef(validate);
|
|
const migrateRef = useRef(migrate);
|
|
defaultValueRef.current = defaultValue;
|
|
validateRef.current = validate;
|
|
migrateRef.current = migrate;
|
|
|
|
const createDefaultValue = useCallback(() => resolveDefaultValue(defaultValueRef.current), []);
|
|
const [value, setValueState] = useState<T>(createDefaultValue);
|
|
const valueRef = useRef(value);
|
|
|
|
const setMemoryValue = useCallback((nextValue: T) => {
|
|
valueRef.current = nextValue;
|
|
setValueState(nextValue);
|
|
}, []);
|
|
|
|
const removeStoredValue = useCallback(() => {
|
|
if (!key) return;
|
|
try {
|
|
window.localStorage.removeItem(key);
|
|
} catch {
|
|
// Storage can be unavailable in private or restricted browser contexts.
|
|
}
|
|
}, [key]);
|
|
|
|
useEffect(() => {
|
|
if (!key) {
|
|
setMemoryValue(createDefaultValue());
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const serialized = window.localStorage.getItem(key);
|
|
if (serialized === null) {
|
|
setMemoryValue(createDefaultValue());
|
|
return;
|
|
}
|
|
const parsed = parseLocalWidgetState(serialized, version, validateRef.current, migrateRef.current);
|
|
if (parsed === undefined) {
|
|
window.localStorage.removeItem(key);
|
|
setMemoryValue(createDefaultValue());
|
|
return;
|
|
}
|
|
setMemoryValue(parsed);
|
|
} catch {
|
|
setMemoryValue(createDefaultValue());
|
|
}
|
|
}, [createDefaultValue, key, setMemoryValue, version]);
|
|
|
|
useEffect(() => {
|
|
if (!key) return;
|
|
|
|
const handleStorage = (event: StorageEvent) => {
|
|
if (event.storageArea !== window.localStorage || event.key !== key) return;
|
|
if (event.newValue === null) {
|
|
setMemoryValue(createDefaultValue());
|
|
return;
|
|
}
|
|
|
|
const parsed = parseLocalWidgetState(event.newValue, version, validateRef.current, migrateRef.current);
|
|
if (parsed !== undefined) setMemoryValue(parsed);
|
|
};
|
|
window.addEventListener("storage", handleStorage);
|
|
return () => window.removeEventListener("storage", handleStorage);
|
|
}, [createDefaultValue, key, setMemoryValue, version]);
|
|
|
|
const setValue: Dispatch<SetStateAction<T>> = useCallback(
|
|
(nextValue) => {
|
|
const resolved =
|
|
typeof nextValue === "function" ? (nextValue as (previous: T) => T)(valueRef.current) : nextValue;
|
|
if (Object.is(resolved, valueRef.current)) return;
|
|
setMemoryValue(resolved);
|
|
if (key) {
|
|
try {
|
|
window.localStorage.setItem(key, serializeLocalWidgetState(version, resolved));
|
|
} catch {
|
|
// Keep the in-memory state usable when persistence is unavailable.
|
|
}
|
|
}
|
|
},
|
|
[key, setMemoryValue, version],
|
|
);
|
|
|
|
const resetValue = useCallback(() => {
|
|
removeStoredValue();
|
|
setMemoryValue(createDefaultValue());
|
|
}, [createDefaultValue, removeStoredValue, setMemoryValue]);
|
|
|
|
return [value, setValue, resetValue];
|
|
};
|