usehooks icon indicating copy to clipboard operation
usehooks copied to clipboard

Add useMemo call to useLocalStorage so that the value returned only changes if store actually changes.

Open alnorth opened this issue 1 year ago • 6 comments

At the moment if a component that uses useLocalStorage rerenders then useLocalStorage will call JSON.parse(store) as a result of the rerender. The value of store won't have changed, but the new call to JSON.parse will mean that a new value is returned with identical contents to the old value. Any hooks then using that value in their deps array will then have to be run again, even though the actual data is the same.

I've updated useLocalStorage so that it uses useMemo to avoid repeated calls to JSON.parse with the same store value.

alnorth avatar May 15 '24 10:05 alnorth

image

our current awful workaround

hope this pr gets merged

joshxyzhimself avatar Jul 01 '24 00:07 joshxyzhimself

We ran into the same problem - somewhat similar with a JWT, on which a some other hooks depend.

I think data in the localStorage might be some basic values (authorization, language) which is used (directly or derived) in a lot of parts of the application. So this trade-off between memory & CPU should be worth using the useMemo().

RobertRad avatar Jul 19 '24 09:07 RobertRad

update on my workaround, no issues so far.

/**
 *
 * If active tab updates localStorage:
 *  - At current tab, localStorage is updated.
 *  - At current tab, "storage" event is emitted.
 *  - At current tab, window receives "storage" event.
 *  - At current tab, state is updated.
 *
 * If inactive tab updates localStorage:
 *  - At inactive tab, localStorage is updated.
 *  - At current tab, window receives "storage" event.
 *  - At current tab, state is updated.
 *
 */

import { useCallback, useEffect, useMemo, useState } from "react";

export function useLocalStorage<T>(key: string) {
  const [state, set_state] = useState<T | null>(() => {
    const unparsed = localStorage.getItem(key);
    if (typeof unparsed === "string") {
      const parsed = JSON.parse(unparsed) as T;
      return parsed;
    }
    return null;
  });

  useEffect(() => {
    const listener = (e: StorageEvent) => {
      if (e.key === key) {
        if (typeof e.newValue === "string") {
          const parsed = JSON.parse(e.newValue) as T;
          set_state(parsed);
        }
      }
    };
    window.addEventListener("storage", listener);
    return () => {
      window.removeEventListener("storage", listener);
    };
  }, [key]);

  /**
   * @description dispatches an event so all instances gets updated.
   */
  const set_stored = useCallback(
    (value: T | null) => {
      const newValue = JSON.stringify(value);
      localStorage.setItem(key, newValue);
      const event = new StorageEvent("storage", { key, newValue });
      dispatchEvent(event);
    },
    [key],
  );

  return useMemo(
    () => [state, set_stored] as [T | null, (value: T | null) => void],
    [state, set_stored],
  );
}

export default useLocalStorage;
import useLocalStorage from "./useLocalStorage";

export interface Session {
  id: string;
  iss: string;
  aud: string;
  sub: string;
  iat: number;
  nbf: number;
  exp: number;
}

export const useSession = () => {
  return useLocalStorage<Session>("session");
};

export default useSession;

joshxyzhimself avatar Jul 19 '24 11:07 joshxyzhimself

This hook should accept an initialValue being passed.

pedroapfilho avatar Jan 22 '25 13:01 pedroapfilho

@lynnandtonic @tylermcginnis Any chance we can get this merged?

yunyu avatar Jan 28 '25 07:01 yunyu

Is this package unmaintained?

RutsuKun avatar Jun 08 '25 18:06 RutsuKun