Installation

To install the package, run the following command:

npm install use-persistent-form
Or

Copy & paste this into your app:

import { useCallback, useEffect } from "react";

import {
  useForm,
  useWatch,
  type UseFormProps,
  type FieldValues,
  type UseFormReturn,
  type DefaultValues,
} from "react-hook-form";

type StorageKey = string | Array<string>;

type AsyncDefaultValues<TFieldValues> = (
  payload?: unknown
) => Promise<TFieldValues>;

type FormDefaultValues<TFieldValues extends FieldValues = FieldValues> =
  | DefaultValues<TFieldValues>
  | AsyncDefaultValues<TFieldValues>;

type UsePersistentFormProps<
  TFieldValues extends FieldValues = FieldValues,
  TContext = any,
> = UseFormProps<TFieldValues, TContext> & {
  storageKey: StorageKey;
  skipStorageValidation?: boolean;
  storage?: Storage;
};

type UsePersistentFormReturn<
  TFieldValues extends FieldValues = FieldValues,
  TContext = any,
  TransformedValues extends FieldValues | undefined = undefined,
> = UseFormReturn<TFieldValues, TContext, TransformedValues> & {
  clearData: () => void;
};

function storageKeyToString(storageKey: StorageKey): string {
  if (Array.isArray(storageKey)) return storageKey.join();

  return storageKey;
}

function getFormDefaultValues<TFieldValues extends FieldValues = FieldValues>(
  key: string,
  storage: Storage,
  initValues?: FormDefaultValues<TFieldValues>
): FormDefaultValues<TFieldValues> | undefined {
  const values = storage.getItem(key);

  if (!values) {
    return undefined;
  }

  const parsed = JSON.parse(values);

  // I think how can I parse the values from storage by schema
  // currently, if schema is provided, it's provided via resolver
  // but through resolver, there is no way to get it.
  // passing schema as another prop feels kind of redundant?
  if (typeof parsed === "object" && !Array.isArray(parsed) && parsed != null) {
    return {
      ...initValues,
      ...parsed,
    };
  }

  return undefined;
}

export function usePersistentForm<
  TFieldValues extends FieldValues = FieldValues,
  TContext = any,
  TTransformedValues extends FieldValues | undefined = undefined,
>({
  storage = typeof window != "undefined" ? sessionStorage : undefined,
  storageKey,
  skipStorageValidation = false,
  defaultValues: initValues,
  ...props
}: UsePersistentFormProps<TFieldValues, TContext>): UsePersistentFormReturn<
  TFieldValues,
  TContext,
  TTransformedValues
> {
  if (!storage)
    throw new Error(
      "usePersistentForm was called on the server. Server doesn't have any access to the storage object."
    );

  const key = storageKeyToString(storageKey);

  const defaultValues = getFormDefaultValues<TFieldValues>(
    key,
    storage,
    initValues
  );

  const form = useForm<TFieldValues, TContext, TTransformedValues>({
    ...props,
    defaultValues,
  });

  const watchedValues = useWatch({
    control: form.control,
  });

  useEffect(() => {
    storage.setItem(key, JSON.stringify(watchedValues));
  }, [watchedValues]);

  const clearData = useCallback(() => {
    storage.removeItem(key);
  }, [key, storage]);

  return { ...form, clearData };
}