Не могу получить доступ к localStorage в Nextjs

Я не могу получить доступ к localStorage в Nextjs. Я могу использовать его в своем коде, и все работает как положено, и в ожидаемой работе ошибок нет. Однако терминал выдает ошибку:

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 |   );

Вот мой код, никакие другие файлы не вызывают этой проблемы:

"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;

Я попытался добавить проверку useEffect, которая должна проверять наличие localStorage, и если она недоступна, назначать значения по умолчанию, но она всегда говорит, что localStorage не определен, даже если это так.

Сервер не знает, что такое window, localstorage, sessionstorage и т. д.

Dom 17.05.2024 00:00
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
1
1
75
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ошибка возникает из-за того, что компоненты клиента и сервера сначала отображаются на сервере. Директива «use client» лишь сообщает, что компонент включает в себя интерактивные элементы, которые должны выполняться на клиенте.

Поэтому элемент окна не определен во время рендеринга сервера, и из-за этого локальное хранилище также недоступно.

Это должно работать нормально, если КАЖДОЕ появление локального хранилища находится в ловушке useEffect, даже те, которые сейчас находятся внутри ловушки useState как значение по умолчанию.

Поэтому убедитесь, что вы никогда не используете локальное хранилище за пределами хука эффекта использования, и тогда все должно работать нормально.

И причина, по которой он все еще работает, заключается в том, что он работает, когда клиент берет на себя управление. Отображаемая ошибка связана с рендерингом на стороне сервера.

Когда я помещаю все возможные вещи, связанные с localStorage, внутрь useEffect и обрабатываю случай, когда localStorage недоступен, он всегда говорит, что он недоступен, и сбрасывает значения по умолчанию. Кроме того, он обновляет значения, просто в начале, когда страница монтируется, у него по какой-то причине нет к ней доступа.

Yevheniia Simaka 17.05.2024 05:54
Ответ принят как подходящий

В вашем коде есть несколько проблем, но давайте разберемся с ними шаг за шагом. Эта часть будет выполнена на client и на server:

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

это эквивалентно:

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

localStorage.getItem("compactView") неявно пытается получить доступ window к тому, что недоступно на сервере. Есть много способов это исправить. Например, мы можем сделать это с помощью оператора цепочки ?. и явного доступа к window:

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

или, что ещё лучше, мы можем извлечь эту логику в функцию:

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

И еще одна вещь, на которую следует обратить внимание: если элемент отсутствует в localStorage, JSON.parse("") всегда будет терпеть неудачу с Uncaught SyntaxError: Unexpected end of JSON input. Итак, давайте исправим это:

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

мы могли бы пойти еще дальше и реализовать собственный хук для обработки логики обновления. Нет необходимости использовать useEffect в компоненте:

"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

Другие вопросы по теме