usehooks
usehooks copied to clipboard
Error: useLocalStorage is a client-only hook
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
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.
Same issue here when we refresh the page with ctrl+r :/
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.
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.
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
I switch to useLocalStorageState from alibaba/hooks solve the problem
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.
What does "server-safe" mean on the homepage if not that the hooks can be executed in a SSR environment?
@gpaiva00 the only missing piece is useSyncExternalStore as this should better with React 18 concurrency and is what the current implementation in this library uses: https://github.com/uidotdev/usehooks/blob/main/index.js#L619
So this is broken since past 1yr and no resolution has been provided?
So this is broken since past 1yr and no resolution has been provided?
nope! you gotta have your own workarounds.
@gpaiva00 I think this:
// Check if we're in a browser environment if (typeof window === "undefined") { return initialValue; }
inside the initializer function of useState does not protect from hydration issues, because initialValue may be different from the value that was used on the server. I've seen the logic that you have in the initializer function put in the useEffect instead, as here - where you can directly read from window.
If you use pnpm you can use pnpm patch to comment out the error.
something like
const getLocalStorageServerSnapshot = () => {
// throw Error("useLocalStorage is a client-only hook");
console.warn("useLocalStorage is a client-only hook");
};
If you use
pnpmyou can usepnpm patchto comment out the error.something like
const getLocalStorageServerSnapshot = () => { // throw Error("useLocalStorage is a client-only hook"); console.warn("useLocalStorage is a client-only hook"); };
I don't need to use pnpm to do that. But yes, if I just comment it out, the error will be gone. But then there comes another error for not finding the value or something. (I didn't work in that project for months, so I don't fully remember the error)
But I do remember, it didn't provide a solution.
I fixed the issue by wrapping the component that uses the useLocalStorage hook in a component the prevents rendering on the server using a useEffect, as directed by the Next.js docs
https://nextjs.org/docs/messages/react-hydration-error#solution-1-using-useeffect-to-run-on-the-client-only
"use client";
import { useEffect, useState } from "react";
export function MyProvider() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
if (!isClient) {
return null;
}
return (
// DataProvider used the useLocalStorage hook
<DataProvider data={data}>
Hello World
</DataProvider>
);
}