7

I have the following custom hook which stores data in the localstorage:

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

export const useLocalStorage = (key, initialValue) => {
  const initialize = (key) => {
    try {
      const item = localStorage.getItem(key);
      if (item && item !== "undefined") {
        return JSON.parse(item);
      }

      localStorage.setItem(key, JSON.stringify(initialValue));
      return initialValue;
    } catch {
      return initialValue;
    }
  };

  const [state, setState] = useState(() => initialize(key)); // problem is here

  const setValue = useCallback(
    (value) => {
      try {
        const valueToStore = value instanceof Function ? value(storedValue) : value;
        setState(valueToStore);
        localStorage.setItem(key, JSON.stringify(valueToStore));
      } catch (error) {
        console.log(error);
      }
    },
    [key, setState]
  );

  const remove = useCallback(() => {
    try {
      localStorage.removeItem(key);
    } catch {
      console.log(error);
    }
  }, [key]);

  return [state, setValue, remove];
};


It shows the following problem, which i googled it and it seems it due to the fact that Nextjs tries to run the code on the server side and there is no available window object.

enter image description here

The problem seems to come from the line where i try to initialize the stored data:

const [state, setState] = useState(() => initialize(key));

I tried to pack this logic within a useEffect so it only runs on client side, but i got the infinite loop which i couldnt solve.

J.Doe
  • 301
  • 1
  • 3
  • 12

3 Answers3

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

export const useLocalStorage = (key, initialValue) => {
  const initialize = (key) => {
    try {
      const item = localStorage.getItem(key);
      if (item && item !== "undefined") {
        return JSON.parse(item);
      }

      localStorage.setItem(key, JSON.stringify(initialValue));
      return initialValue;
    } catch {
      return initialValue;
    }
  };

  const [state, setState] = useState(null); // problem is here

  // solution is here....
  useEffect(()=>{
    setState(initialize(key));
  },[]);

  const setValue = useCallback(
    (value) => {
      try {
        const valueToStore = value instanceof Function ? value(storedValue) : value;
        setState(valueToStore);
        localStorage.setItem(key, JSON.stringify(valueToStore));
      } catch (error) {
        console.log(error);
      }
    },
    [key, setState]
  );

  const remove = useCallback(() => {
    try {
      localStorage.removeItem(key);
    } catch {
      console.log(error);
    }
  }, [key]);

  return [state, setValue, remove];
};

window,localStorage,sessionStorage,etc.. are not defined on server, so accessing them on server will result as an error. Use useEffect to make sure that, these code will be executed on client side.

Rahul
  • 1,858
  • 1
  • 12
  • 33
  • ```React Hook useEffect has missing dependencies: 'initialize' and 'key'.``` So when i add initialzie method to the depenedncy, it says that the initialize method should use useCallback, so than i made it using that. When i add initialValue to the dependency array of useCallback, i got infinite loop. – J.Doe Aug 17 '21 at 05:17
  • Do not add anythinff – Rahul Aug 17 '21 at 10:02
  • Is it a good practice to leave out dependency? – J.Doe Aug 17 '21 at 10:23
  • @J.Doe `useEffect` completely runs on client side, so you don't need to add any dependency. – Rahul Aug 18 '21 at 01:05
  • https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies – J.Doe Aug 18 '21 at 08:16
  • If you want rerender then, otherwise it is completely okey – Rahul Aug 18 '21 at 11:24
0

Maybe move the initialize to inside a useEffect (useState needs to be kept outside however)

Inside the useEffect, you initialize only if typeof window !== "undefined"

Bobby Connolly
  • 317
  • 2
  • 9
0

This worked for me with React/NextJS :)

import { useEffect, useState } from 'react';

const isServer = typeof window === 'undefined';

export default function useLocalStorage(key, initialValue) {
  // State to store our value
  // Pass initial state function to useState so logic is only executed once
  const [storedValue, setStoredValue] = useState(() => initialValue);

  const initialize = () => {
    if (isServer) {
      return initialValue;
    }
    try {
      // Get from local storage by key
      const item = window.localStorage.getItem(key);
      // Parse stored json or if none return initialValue
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      // If error also return initialValue
      console.log(error);
      return initialValue;
    }
  };

  /* prevents hydration error so that state is only initialized after server is defined */
  useEffect(() => {
    if (!isServer) {
      setStoredValue(initialize());
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Return a wrapped version of useState's setter function that ...
  // ... persists the new value to localStorage.
  const setValue = (value) => {
    try {
      // Allow value to be a function so we have same API as useState
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      // Save state
      setStoredValue(valueToStore);
      // Save to local storage
      if (typeof window !== 'undefined') {
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      }
    } catch (error) {
      // A more advanced implementation would handle the error case
      console.log(error);
    }
  };
  return [storedValue, setValue];
}
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Apr 01 '23 at 04:51