Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error: useLocalStorage is a client-only hook #318

Open
Nusab19 opened this issue Jul 25, 2024 · 7 comments
Open

Error: useLocalStorage is a client-only hook #318

Nusab19 opened this issue Jul 25, 2024 · 7 comments

Comments

@Nusab19
Copy link

Nusab19 commented Jul 25, 2024

I'm using useLocalStorage hook from @uidotdev/usehooks. Even though HomePage.tsx file is set to "use client", I'm getting the following error:

 ⨯ Error: useLocalStorage is a client-only hook
    at HomePage (./src/components/HomePage.tsx:15:98)
digest: "1786536127"
 GET / 500 in 180ms

Even though the readme states that the hooks are "server-safe", I keep getting this error.

The minimal reproduceable code can be found at: https://github.com/Nusab19/mwc-useLocalStorageHook

The responsible code:

"use client";
import { useLocalStorage } from "@uidotdev/usehooks";
import { Button } from "./ui/button";
import { Separator } from "./ui/separator";

const HomePage = () => {
  const [count, setCount] = useLocalStorage("count", 0); // here's the problem

  return (
    <div className="...">
      <header className="...">Home Page</header>
      <Separator />
      <header className="...">Count: {count}</header>

      <div className="...">
        <Button onClick={() => setCount((prev) => prev + 1)}>Increment</Button>
        <Button onClick={() => setCount((prev) => prev - 1)}>Decrement</Button>
        <Button onClick={() => setCount(0)}>Reset</Button>
      </div>
    </div>
  );
};

export default HomePage;

I also posted a question in stackoverflow

@ThePythoniousGuy
Copy link

Had the same issue in my project. Couldn't find a solution.

So I wrote my own implementation of the useLocalStorage hook.
It uses the default value at first. And a useEffect then changes the state to the local storage's value.

The problem with it is, it gives a flicker each time you refresh the page.

@GeekPress
Copy link

GeekPress commented Jul 28, 2024

Same issue here when we refresh the page with ctrl+r :/

@Nusab19
Copy link
Author

Nusab19 commented Jul 28, 2024

Same issue here when we refresh the page with ctrl+r :/

I have identified the issue. The thing is, localSorage is only available in the client side.

But next.js tries to call it from server ( idk why ). That's the reason it is throwing the error.

I don't understand why would next.js call it from server side when I explicitly made the component "use client".

Here's the code that is raising the error.
image

@iamriot-dev
Copy link

iamriot-dev commented Aug 6, 2024

I don't understand why would next.js call it from server side when I explicitly made the component "use client".

"use client" does not mean that the component will not run on the server, SSR will still happen unless specifically disabled, this is intended behaviour. You can look into methods of disabling SSR for specific components on Next.js, if that is what you want to do.

The issue here is that useLocalStorage isn't "server safe", exactly because of the code you screenshotted. According to React docs, that function should be returning the value to be used during server rendering (instead of throwing an error), but this isn't possible since localStorage cannot be accessed by the server (the data is stored only on the browser). The only alternative is to use a fallback value (such as 0 in your case) on the server, which will differ from the actual value stored in the browser.

Unfortunately, the flicker that the alternative implementations have is largely unavoidable, for this very same reason.

@hyper-dot
Copy link

You can use dynamic imports.
https://nextjs.org/learn-pages-router/seo/improve/dynamic-import-components#:~:text=import%20dynamic%20from%20%27next/dynamic%27%3B

@twoguyslabs
Copy link

I switch to useLocalStorageState from alibaba/hooks solve the problem

@gpaiva00
Copy link

Hey guys! I've had the same issue. So I created my custom hook useLocalStorage with the same functionalities from @uidotdev/usehooks. Now I can use it without any problemas in Next.js:

"use client";

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

function useLocalStorage<T>(
  key: string,
  initialValue: T,
): [T, (value: T | ((val: T) => T)) => void] {
  // Initialize state with a function to handle SSR
  const [localState, setLocalState] = useState<T>(() => {
    // Check if we're in a browser environment
    if (typeof window === "undefined") {
      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) {
      console.warn(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });

  // Return a wrapped version of useState's setter function that persists the new value to localStorage
  const handleSetState = useCallback(
    (value: T | ((val: T) => T)) => {
      try {
        // Allow value to be a function so we have same API as useState
        const valueToStore =
          value instanceof Function ? value(localState) : value;
        // Save state
        setLocalState(valueToStore);
        // Save to local storage
        if (typeof window !== "undefined") {
          window.localStorage.setItem(key, JSON.stringify(valueToStore));
        }
      } catch (error) {
        console.warn(`Error setting localStorage key "${key}":`, error);
      }
    },
    [key, localState],
  );

  useEffect(() => {
    // Handle storage changes in other tabs/windows
    function handleStorageChange(event: StorageEvent) {
      if (event.key === key && event.newValue) {
        setLocalState(JSON.parse(event.newValue));
      }
    }

    // Subscribe to storage changes
    if (typeof window !== "undefined") {
      window.addEventListener("storage", handleStorageChange);
    }

    // Cleanup the event listener on component unmount
    return () => {
      if (typeof window !== "undefined") {
        window.removeEventListener("storage", handleStorageChange);
      }
    };
  }, [key]);

  return [localState, handleSetState];
}

export { useLocalStorage };

Hope I've helped you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants