Reputation: 51
I am trying to create a replacement of useState
in next.js resilient to page refreshes.
One of the possible solutions that came across was to use window.localStorage
to save and retrieve the state. That would make state persistent even after page refreshes.
I found the following implementation of a useLocalStorage
hook for ReactJS https://usehooks.com/useLocalStorage/
function useLocalStorage(key, initialValue) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
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) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};
return [storedValue, setValue];
}
However, it generates the following error when I use it in NextJS:
Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server
After a little search I found out that the window
object doesn't exist in the (Next.js) server side and this is the possible cause of the error (Window is not defined in Next.js React app).
A possible solution is to protect the usage of window
with the useEffect
hook, that only runs on the client side.
My current implementation of the useLocalStorage
hook is
function useLocalStorage<T>(key: string, defaultValue: T): [T, Dispatch<SetStateAction<T>>] {
const [value, setValue] = useState<T>(defaultValue);
useEffect(() => {
try {
const item = window.localStorage.getItem(key);
setValue(item ? JSON.parse(item) : defaultValue);
}
catch (error) {
setValue(defaultValue);
}
}, [key, defaultValue]);
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
However, this time the hook doesn't always work as expected because the order of execution of the useEffect callbacks is not guaranteed. As a consequence, sometimes the state is lost.
I want to know what would be a correct implementation for this in NextJS and understand where the logic of my code is failing.
Upvotes: 5
Views: 10603
Reputation: 2327
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
export default function useLocalStorage<T>(
key: string,
defaultValue: T
): [T, Dispatch<SetStateAction<T>>] {
const isMounted = useRef(false)
const [value, setValue] = useState<T>(defaultValue)
useEffect(() => {
try {
const item = window.localStorage.getItem(key)
if (item) {
setValue(JSON.parse(item))
}
} catch (e) {
console.log(e)
}
return () => {
isMounted.current = false
}
}, [key])
useEffect(() => {
if (isMounted.current) {
window.localStorage.setItem(key, JSON.stringify(value))
} else {
isMounted.current = true
}
}, [key, value])
return [value, setValue]
}
Here, useRef
is being used to prevent the defaultValue
getting stored in localStorage
in first render. Basically, it skips the callback of second useEffect
to run on first render, so initialization can complete without race condition by first useEffect
hook.
Upvotes: 7
Reputation: 11
I don't know if the author is still relevant. But maybe someone will come in handy.
This worked for me. I replaced one useEffect
with a function so that useEffect
wouldn't get called uncontrollably.
The default value will be set to useEffect
, in all other cases the changeValue
function will be called.
export const useLocalStorage = (key: string, defaultValue: any) => {
const [value, setValue] = useState(defaultValue);
const changeValue = (value) => {
setValue(value);
localStorage.setItem(key, JSON.stringify(value));
}
useEffect(() => {
const stored = localStorage.getItem(key);
if (!stored) {
setValue(defaultValue);
localStorage.setItem(key, JSON.stringify(defaultValue));
} else {
setValue(JSON.parse(stored));
}
}, [defaultValue, key]);
return [value, changeValue]
}
Upvotes: 1
Reputation: 43
This worked for me: Next.js use localstorage problem with SSR
... and it didn't reset the value at every refresh unlike @MarkosTh09's solution.
Upvotes: 0
Reputation: 133
Markos's answer kept resetting the state on refresh for me. I added one small tweak, and that fixed the issue:
import React, { useDebugValue, useEffect, useState } from "react"
const useLocalStorage = <S>(
key: string,
initialState?: S | (() => S)
): [S, React.Dispatch<React.SetStateAction<S>>] => {
const [state, setState] = useState<S>(initialState as S)
useDebugValue(state)
useEffect(() => {
const item = localStorage.getItem(key)
if (item) setState(parse(item))
}, [])
useEffect(() => {
if (state !== initialState) {
localStorage.setItem(key, JSON.stringify(state))
}
}, [state])
return [state, setState]
}
const parse = (value: string) => {
try {
return JSON.parse(value)
} catch {
return value
}
}
export default useLocalStorage
Upvotes: 2
Reputation: 334
Here is a useLocalStorage hook which works with next.js
import React, { useDebugValue, useEffect, useState } from "react";
export const useLocalStorage = <S>(
key: string,
initialState?: S | (() => S)
): [S, React.Dispatch<React.SetStateAction<S>>] => {
const [state, setState] = useState<S>(initialState as S);
useDebugValue(state);
useEffect(() => {
const item = localStorage.getItem(key);
if (item) setState(parse(item));
}, []);
useEffect(() => {
localStorage.setItem(key, JSON.stringify(state));
}, [state]);
return [state, setState];
};
const parse = (value: string) => {
try {
return JSON.parse(value);
} catch {
return value;
}
};
Upvotes: 2