All files / libs/ext/react/hooks/src/lib/use-state-in-storage use-state-in-storage.ts

91.48% Statements 86/94
84.61% Branches 11/13
100% Functions 3/3
91.48% Lines 86/94

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 951x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 4x 4x 4x 2x 1x 1x 4x 4x 1x 1x 4x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 4x 4x 4x 5x 5x 5x               5x 5x 5x 5x 5x 5x 5x 5x   5x 5x 5x 5x  
import { hashKey } from '@tanstack/react-query';
import { useCallback, useEffect, useRef, useState, type Dispatch, type SetStateAction } from 'react';
import { type ZodType } from 'zod';
 
import { useUpdateEffect } from '../use-update-effect/useUpdateEffect';
 
const isStateFn = <TState>(setStateAction: unknown): setStateAction is (prevState?: TState) => TState =>
  typeof setStateAction === 'function';
 
// Helper function to get a value from storage and validate it against a schema.
const getFromStorage = <TState>(key: string, storage: Storage, validationSchema?: ZodType<TState>) => {
  const storageState = storage.getItem(key);
  try {
    return storageState
      ? validationSchema
        ? validationSchema.parse(JSON.parse(storageState))
        : (JSON.parse(storageState) as TState)
      : undefined;
  } catch {
    return undefined;
  }
};
 
export function useStateInStorage<TState>(
  key: unknown[] | string,
  initialState: TState | ((stateFromStorage?: TState) => TState),
  options?: {
    storage?: Storage;
    validationSchema?: ZodType<TState>;
  },
): [state: TState, setState: Dispatch<SetStateAction<TState>>, clear: () => void];
 
export function useStateInStorage<TState = undefined>(
  key: unknown[] | string,
  initialState?: undefined,
  options?: {
    storage?: Storage;
    validationSchema?: ZodType<TState>;
  },
): [state: TState | undefined, setState: Dispatch<SetStateAction<TState | undefined>>, clear: () => void];
 
export function useStateInStorage<TState>(
  /** Key in storage. Will be hashed using react-query's hashing method. */
  key: unknown[] | string,
  /** Initial state. If there is a value in storage, it is ignored. You can pass a function to get the value from storage (if any) and return the computed state. */
  initialState?: TState | ((statementFromStorage?: TState) => TState),
  {
    storage = localStorage,
    validationSchema,
  }: {
    /** Override storage (defaults to localStorage). */
    storage?: Storage;
    /**
     * Optional validation schema. If the value in storage does not match the schema, it will be ignored.
     * You can use this to invalidate the storage after a schema change.
     */
    validationSchema?: ZodType<TState>;
  } = {},
): [state: TState | undefined, setState: Dispatch<SetStateAction<TState | undefined>>, clear: () => void] {
  const hashedKey = hashKey(Array.isArray(key) ? key : [key]);
 
  const initialStateRef = useRef(initialState);
  initialStateRef.current = initialState;
 
  const validationSchemaRef = useRef(validationSchema);
  validationSchemaRef.current = validationSchema;
 
  const [state, setState] = useState(() => {
    // Get the state from storage if any. If it doesn't match the schema, it will be undefined (as if there was no value).
    const stateInStorage = getFromStorage(hashedKey, storage, validationSchema);
    return isStateFn<TState>(initialState) ? initialState(stateInStorage) : (stateInStorage ?? initialState);
  });
 
  useUpdateEffect(() => {
    // Get the state from storage if any. If it doesn't match the schema, it will be undefined (as if there was no value).
    const stateInStorage = getFromStorage(hashedKey, storage, validationSchemaRef.current);
    setState(
      isStateFn<TState>(initialStateRef.current)
        ? initialStateRef.current(stateInStorage)
        : (stateInStorage ?? initialStateRef.current),
    );
  }, [hashedKey, storage]);
 
  // When the state changes, persist it in local storage.
  useEffect(() => {
    storage.setItem(hashedKey, JSON.stringify(state));
  }, [state, hashedKey, storage]);
 
  const handleClear = useCallback(() => {
    storage.removeItem(hashedKey);
  }, [hashedKey, storage]);
 
  return [state, setState, handleClear];
}