Next.js Use localStorage For Rendering Without Flash?

Hello, I am trying to grab data from localStorage and use it to change the text in a button. This system should detect if a user has clicked the button before and say "continue" instead of "start." I made a useUserHistory hook to wrap this behaviour.
import { useEffect, useReducer, useState } from "react";

export type UserHistoryObject = {
[key: string]: string;
};

export interface UserHistory {
userHistory: UserHistoryObject;
setUserHistory: (pv: UserHistoryObject) => void;
getBoolean: (item: string) => boolean;
setBoolean: (item: string, value: boolean) => void;
getNumber: (item: string) => number;
setNumber: (item: string, value: number) => void;
getString: (item: string) => string;
setString: (item: string, value: string) => void;
clearUserHistory: () => void;
}

export default function useUserHistory(): UserHistory {
const [userHistory, setUserHistory] = useState({} as any);

useEffect(() => {
if (
localStorage.getItem("userHistory") &&
Object.keys(userHistory).length === 0
) {
setUserHistory(JSON.parse(localStorage.getItem("userHistory") as string));
}
}, []);

useEffect(() => {
if (
localStorage.getItem("userHistory") &&
Object.keys(userHistory).length === 0
)
return;
localStorage.setItem("userHistory", JSON.stringify(userHistory));
}, [userHistory]);

return {
userHistory,
setUserHistory,
getBoolean: (item) => userHistory?.[item] === "true" || false,
setBoolean: (item, value) => {
const newUserHistory = { ...userHistory };
newUserHistory[item] = String(value);
setUserHistory(newUserHistory);
},
getNumber: (item) => Number(userHistory?.[item]),
setNumber: (item, value) => {
const newUserHistory = { ...userHistory };
newUserHistory[item] = String(value);
setUserHistory(newUserHistory);
},
getString: (item) => userHistory?.[item],
setString: (item, value) => {
const newUserHistory = { ...userHistory };
newUserHistory[item] = value;
setUserHistory(newUserHistory);
},
clearUserHistory: () => {
setUserHistory({});
},
};
}
import { useEffect, useReducer, useState } from "react";

export type UserHistoryObject = {
[key: string]: string;
};

export interface UserHistory {
userHistory: UserHistoryObject;
setUserHistory: (pv: UserHistoryObject) => void;
getBoolean: (item: string) => boolean;
setBoolean: (item: string, value: boolean) => void;
getNumber: (item: string) => number;
setNumber: (item: string, value: number) => void;
getString: (item: string) => string;
setString: (item: string, value: string) => void;
clearUserHistory: () => void;
}

export default function useUserHistory(): UserHistory {
const [userHistory, setUserHistory] = useState({} as any);

useEffect(() => {
if (
localStorage.getItem("userHistory") &&
Object.keys(userHistory).length === 0
) {
setUserHistory(JSON.parse(localStorage.getItem("userHistory") as string));
}
}, []);

useEffect(() => {
if (
localStorage.getItem("userHistory") &&
Object.keys(userHistory).length === 0
)
return;
localStorage.setItem("userHistory", JSON.stringify(userHistory));
}, [userHistory]);

return {
userHistory,
setUserHistory,
getBoolean: (item) => userHistory?.[item] === "true" || false,
setBoolean: (item, value) => {
const newUserHistory = { ...userHistory };
newUserHistory[item] = String(value);
setUserHistory(newUserHistory);
},
getNumber: (item) => Number(userHistory?.[item]),
setNumber: (item, value) => {
const newUserHistory = { ...userHistory };
newUserHistory[item] = String(value);
setUserHistory(newUserHistory);
},
getString: (item) => userHistory?.[item],
setString: (item, value) => {
const newUserHistory = { ...userHistory };
newUserHistory[item] = value;
setUserHistory(newUserHistory);
},
clearUserHistory: () => {
setUserHistory({});
},
};
}
Only this version results in no error, but it always flashes "Start" until the value is retrieved. When I use
const [userHistory, setUserHistory] = useState(JSON.parse(localStorage.getItem("userHistory") || "{}"));
const [userHistory, setUserHistory] = useState(JSON.parse(localStorage.getItem("userHistory") || "{}"));
and remove the first useEffect, it seems to work, but I do get a error for localStorage not being defined. I understand this is due to the server pre-rendering the initial value of useState, a behaviour I do not want in this case. Should I rethink this entire system?
4 Replies
Neto
Neto3mo ago
the component renders first on the server where you don't have the local storage and the useeffect runs after the component is rendered the first time you have to use cookies or something similar
Eldon
EldonOP3mo ago
There has to, or atleast should be, a way to do this completely client side. The server overhead for something so small is not really worth it.
Eve
Eve3mo ago
if the little flash of "start" really bothers you, or you need to be 100% certain that the user doesn't click the button in the wrong state, render the button as hidden initially, and only update the visibility when the localStorage state. becomes available
none23
none233mo ago
Basically, you can't totally avoid the flash, because the SSR page will never have localStorage. If the component is not the LCP element on the page, I'd just render null or a placeholder in the component that renders it. E.g. using a mounted hack:
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])

if (!mounted) {
return null // or a placeholder
}
return <ComponentThatReliesOnLocalStorage />
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])

if (!mounted) {
return null // or a placeholder
}
return <ComponentThatReliesOnLocalStorage />
Want results from more Discord servers?
Add your server