skip to Main Content

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.

2

Answers


  1. The Error occurs because Client and Server components are both rendered on the server first. The “use client” directive only tells next that the component includes interactive elements that are to be executed on the client.

    Therefore the window element is undefined during the server rendering and due to that localstorage is also unavailable.

    It should work fine if EVERY occurrence of local storage is in a useEffect hook, even the ones that are now inside the useState hook as default value.

    So make sure that you never use local storage outside of the use effect hook and then it should work just fine.

    And the reason why it is still working is, that it works when the client takes over. The displayed error is from the server side render.

    Login or Signup to reply.
  2. 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
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search