reactjstypescriptnext.jslocal-storage

Can't access the localStorage in Nextjs


I am not able to access the localStorage in Nextjs. I can use it in my code and everything works as expected and no errors are present in the expected work. However the terminal gives the error:

ReferenceError: localStorage is not defined
    at MainGrid (./app/components/WeightDisplay/MainGrid.tsx:17:96)
   8 |   // Initialize states for preferences and retrieve from local storage
   9 |   const [metricKg, setMetricKg] = useState(
> 10 |     JSON.parse(localStorage.getItem("metricKg") || "") || false
     |               ^
  11 |   );

Here is my code, no other files are causing that issue:

"use client";
import React, { useEffect, useState } from "react";
import Entry from "./Entry";
import useEntryModal from "@/app/hooks/useEntryModal";

const MainGrid = () => {
  // Initialize states for preferences and retrieve from local storage
  const [metricKg, setMetricKg] = useState(
    JSON.parse(localStorage.getItem("metricKg") || "") || false
  );
  const [metricCm, setMetricCm] = useState(
    JSON.parse(localStorage.getItem("metricCm") || "") || false
  );
  const [compactView, setCompactView] = useState(
    JSON.parse(localStorage.getItem("compactView") || "") || false
  );

  let entry = {
    num: 0,
    date: "Apr 4th",
    weight: 52, // kg by default
    waist: 50, // cm by default
    hip: 30, // cm by default
  };

  const entryModal = useEntryModal();

  const toggleWeightUnit = () => {
    setMetricKg((prevMetricKg: any) => !prevMetricKg);
  };

  const toggleLengthUnit = () => {
    setMetricCm((prevMetricCm: any) => !prevMetricCm);
  };

  const toggleViewMode = () => {
    setCompactView((prevCompactView: any) => !prevCompactView);
  };

  // Save preferences to local storage whenever they change
  useEffect(() => {
    localStorage.setItem("metricKg", JSON.stringify(metricKg));
  }, [metricKg]);

  useEffect(() => {
    localStorage.setItem("metricCm", JSON.stringify(metricCm));
  }, [metricCm]);

  useEffect(() => {
    localStorage.setItem("compactView", JSON.stringify(compactView));
  }, [compactView]);

  return (
    <div className="flex flex-col w-[90%] mx-auto p-4 gap-0 text-white">
      <div className="flex flex-col">
        <h1 className="text-7xl milky-walky text-white text-center w-fit mx-auto jura my-6 mb-0 relative">
          Entry History
          <button
            onClick={() => entryModal.onOpen()}
            className="absolute top-[50%] translate-y-[-50%] right-[-5rem] centrion"
          >
            +
          </button>
        </h1>
        <div className="flex space-x-2 gap-0">
          <div className="m-6 flex gap-3 border-r-2 border-neutral-600 py-2 pr-8">
            Lbs
            <label htmlFor="one">
              <input
                id="one"
                type="checkbox"
                onClick={toggleWeightUnit}
                checked={metricKg}
              />
            </label>
            Kgs
          </div>
          <div className="m-6 flex gap-3 border-r-2 border-neutral-600 py-2 pr-8">
            Inch
            <label htmlFor="one">
              <input
                id="one"
                type="checkbox"
                onClick={toggleLengthUnit}
                checked={metricCm}
              />
            </label>
            Cm
          </div>
          {/* View mode toggle */}
          <div className="m-6 flex gap-3  py-2 pl-6">
            Compact
            <label htmlFor="compactToggle">
              <input
                id="compactToggle"
                type="checkbox"
                onClick={toggleViewMode}
                checked={!compactView}
              />
            </label>
            Comfortable
          </div>
        </div>
      </div>
      {/* Apply gap based on the view mode */}
      <div
        className={` bg-neutral-800/75 flex flex-col ${
          compactView ? " py-2" : "gap-0"
        }`}
      >
        <Entry
          entry={entry}
          metricKg={metricKg}
          compactView={compactView}
          metricCm={metricCm}
        />
        <Entry
          entry={entry}
          metricKg={metricKg}
          compactView={compactView}
          metricCm={metricCm}
        />
        <Entry
          entry={entry}
          metricKg={metricKg}
          compactView={compactView}
          metricCm={metricCm}
        />
      </div>
    </div>
  );
};

export default MainGrid;

I tryed adding the useEffect check, which sould check for localStorage and if it's not available assign default values, but it always says that localStorage is not defined, even when it is.


Solution

  • There are several problems in your code, but lets get to it step by step. This part will be executed on the client and on the server:

      const [metricKg, setMetricKg] = useState(
        JSON.parse(localStorage.getItem("metricKg") || "") || false // <--
      );
    

    it is an equivalent to:

      const initalMetricKg = JSON.parse(localStorage.getItem("metricKg") || "") || false;
      const [metricKg, setMetricKg] = useState(initalMetricKg);
    

    localStorage.getItem("compactView") implicitly trying to access window that does not available on server. There are many ways to fix it. For example we can do it with chaining operator ?. and explicit access to window:

      const initalMetricKg = JSON.parse(window?.localStorage?.getItem("metricKg") || "") || false;
      const [metricKg, setMetricKg] = useState(initalMetricKg);
    

    or, even better, we can extract this logic to the function:

    function tryGetJsonFromLocalStorage(key, defaultValue) {
       return JSON.parse(window?.localStorage?.getItem(key) || "") || defaultValue;
    }
    
    const MainGrid = () => {
      // Initialize states for preferences and retrieve from local storage
      const [metricKg, setMetricKg] = useState(
        tryGetJsonFromLocalStorage("metricKg", false)
      );
      .. the rest of your code
    

    And one more thing to notice, is that when item is not present in localStorage JSON.parse("") will always fail with Uncaught SyntaxError: Unexpected end of JSON input. So let's fix it:

    function tryGetJsonFromLocalStorage(key, defaultValue) {
       const str = window?.localStorage?.getItem(key);
       if(!str) {
         return defaultValue;
       }
       return JSON.parse(str) || defaultValue;
    }
    
    const MainGrid = () => {
      // Initialize states for preferences and retrieve from local storage
      const [metricKg, setMetricKg] = useState(
        tryGetJsonFromLocalStorage("metricKg", false)
      );
      .. the rest of your code
    

    we could take it even further and implement custom hook to handle update logic. No need to have useEffect in the component:

    "use client";
    import React, { useEffect, useState } from "react";
    import Entry from "./Entry";
    import useEntryModal from "@/app/hooks/useEntryModal";
    
    function tryGetJsonFromLocalStorage(key, defaultValue) {
       const str = window?.localStorage?.getItem(key);
       if(!str) {
         return defaultValue;
       }
       return JSON.parse(str) || defaultValue;
    }
    
    function useLocalStorageState(key, initialValue) {
      const [value, setValue] = useState(
        tryGetJsonFromLocalStorage(key, initialValue)
      );
    
      useEffect(() => {
        // useEffect will never execute on the server but we can be extra-safe here :)
        window?.localStorage?.setItem(key, JSON.stringify(value));
      }, [value]);
    
      return [value, setValue];
    }
    
    const MainGrid = () => {
      // Initialize states for preferences and retrieve from local storage
      const [metricKg, setMetricKg] = useLocalStorageState("metricKg", false);
      const [metricCm, setMetricCm] = useLocalStorageState("metricCm", false);
      const [compactView, setCompactView] = useLocalStorageState("compactView", false);
    
      const toggleWeightUnit = () => {
        setMetricKg((prevMetricKg: any) => !prevMetricKg);
      };
    
      const toggleLengthUnit = () => {
        setMetricCm((prevMetricCm: any) => !prevMetricCm);
      };
    
      const toggleViewMode = () => {
        setCompactView((prevCompactView: any) => !prevCompactView);
      };
    
      let entry = {
        num: 0,
        date: "Apr 4th",
        weight: 52, // kg by default
        waist: 50, // cm by default
        hip: 30, // cm by default
      };
    
      const entryModal = useEntryModal();
      ... rest of your code without useEffects