Bin
2025-12-17 2b99d77d73ba568beff0a549534017caaad8a6de
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import { useCallback, useMemo, useState } from "react";
 
type ValueDecode<T> = (value: string, defaultValue: T) => T;
 
type ValueEncode<T> = (value: T) => string;
 
type Options<T> = {
  decoder?: ValueDecode<T>;
  encoder?: ValueEncode<T>;
};
 
type NextState<T> = T | ((x: T) => T);
type StateResult<T> = [T, (value: NextState<T>) => void];
 
/**
 * Analogous to useState, but persists the state to localStorage
 * @param key {string} - The key to use for localStorage
 * @param defaultValue {T} - The default value to use if the key is not set
 * @param options {Options<T>} - Optional decoder and encoder functions
 * @returns {StateResult<T>} - A tuple containing the current state and a function to update it, the same as useState
 */
export const usePersistentState = <T>(key: string, defaultValue: T, options: Options<T> = {}): StateResult<T> => {
  const { decoder, encoder } = options;
  const initialState: T = useMemo(() => {
    const localStorageValue = localStorage.getItem(key);
 
    return typeof localStorageValue === "string"
      ? (decoder?.(localStorageValue, defaultValue) ?? (localStorageValue as T))
      : defaultValue;
  }, []);
  const [value, setValue] = useState<T>(initialState);
  const setPersistentValue = useCallback((value: NextState<T>) => {
    setValue((prevValue: T) => {
      const newValue = value instanceof Function ? value(prevValue) : value;
      const convertedValue = encoder?.(newValue) ?? (newValue as any).toString();
 
      localStorage.setItem(key, convertedValue);
      return newValue;
    });
  }, []);
 
  return [value, setPersistentValue];
};
 
function jsonDecoder<T>(value: string, defaultValue: T): T {
  try {
    return JSON.parse(value) as T;
  } catch {
    return defaultValue;
  }
}
 
function jsonEncoder<T>(value: T): string {
  return JSON.stringify(value);
}
 
/**
 * Analogous to useState, but persists the state to localStorage as JSON
 * @param key {string} - The key to use for localStorage
 * @param defaultValue {T} - The default value to use if the key is not set
 * @returns {StateResult<T>} - A tuple containing the current state and a function to update it, the same as useState
 */
export const usePersistentJSONState = <T>(key: string, defaultValue: T): StateResult<T> => {
  return usePersistentState<T>(key, defaultValue, { decoder: jsonDecoder, encoder: jsonEncoder });
};