Client component hydration error
Answered
Savannah posted this in #help-forum
SavannahOP
Hi! I'm working on my portfolio developer website and right now I'm working on "Facts about me" section. I'm creating a "FactCard" component that can switch displayed fact on it. The thing is, I am saving the index of last seen fact by user in his localStorage and I want to specifically display the latest fact. Right now I'm loading data in my useState like so:
But it gives me either "localStorage is not defined error" or "hydration error" because server always renders first fact but client renders latest it's seen.
Any help will be appreciated! Using app router w/NextJS 14.1.4
const [currentFact, setCurrentFact] = useState<number>(() => {
// Need to get data from localstorage before rendering.
// If i fetch it like this:
// if (typeof localStorage === "undefined") {
// return 0;
// }
// It gives hydration error because the output is different
// from the server render :/
return parseInt(localStorage.getItem("fact-index") || "0");
});But it gives me either "localStorage is not defined error" or "hydration error" because server always renders first fact but client renders latest it's seen.
Any help will be appreciated! Using app router w/NextJS 14.1.4
Answered by Toyger
for access on server theoretically you can save data to cookies and prehydrate from there
43 Replies
Toyger
if you don't have access to this data on server and it exist only on client, then set default value to 0 and change value to localstorage one in useEffect on mount
@Toyger if you don't have access to this data on server and it exist only on client, then set default value to 0 and change value to localstorage one in useEffect on mount
SavannahOP
Is there a way to prevent the flash of default fact? It firstly shows me the first fact and only then switches
With hydration error it was instant though...
And also why does it show me this?
useEffect(() => {
const savedFact = parseInt(localStorage.getItem("fact-index") || "0")
if (savedFact !== currentFact) {
setCurrentFact(savedFact)
}
}, [])Toyger
you can hide it by default and reveal only in useEffect when you applied data.
you can ignore this warning on screenshot
you can ignore this warning on screenshot
SavannahOP
And this is the only way yeah? :/
It will cause some layout shift if I hide it first and then display
Toyger
you can set it to opacity:0 and 1 for reveal.
it's the easiest way, otherwise you need to have access to this data on server to pre-hydrate it on page
it's the easiest way, otherwise you need to have access to this data on server to pre-hydrate it on page
Toyger
for access on server theoretically you can save data to cookies and prehydrate from there
Answer
SavannahOP
Cookies, damn
Alright, thank you for answers! I'll see which one is best for me
SavannahOP
@Toyger 🥲
Then I guess only useEffect() is possible after all because the fact chooser component requires "use client" because of buttons
@Savannah Then I guess only useEffect() is possible after all because the fact chooser component requires "use client" because of buttons
Toyger
theoretically you can, but I don't know how you build your page, if you separate client only code to separate component you still can use hydration.
SavannahOP
Here it is
I think I abstracted it enough. It'd be strange to separate the <p></p> tag that displays current fact out of the component itself
Toyger
is it page or compoennt?
@Toyger is it page or compoennt?
SavannahOP
Component
Toyger
then in page itself get value from cookies and pass it to component as pre-hydrated value
SavannahOP
Hmm, I don't think it will work?.. But let me try...
@Toyger then in page itself get value from cookies and pass it to component as pre-hydrated value
SavannahOP
How can I save it to cookies then? I'm switching facts on button click, but those event handlers available only in client component
Getting data from cookies is possible, I agree
@Savannah How can I save it to cookies then? I'm switching facts on button click, but those event handlers available only in client component
Toyger
call fetch to some route handler that will save it to cookies
SavannahOP
I got it, alright
Never worked with them, today will be my first time
If I get any more problems, I'll tell ya. But for everything else, thank you so much. Levelled up my experience a bit 😁
@Toyger call fetch to some route handler that will save it to cookies
SavannahOP
Is it important to do so via route handlers? I thought about using server actions, but I'm receiving this error on fact switch (setting state)
saveFact() is a server action that sets a cookie. It also errors with "setstate-in-render" when I wrap it in like so:
(async () => {
saveFact(newIndex)
})()definition:
"use server"
import { cookies } from "next/headers"
export default async function saveFact(index: number) {
cookies().set("latestFact", index.toString())
}SavannahOP
incrementFactBy() gets called in onClick handlers, yesbut the
saveFact() function itself gets called in setCurrentFact() which is a state setterwhat do you mean inside setCurrentFact?
@Savannah Click to see attachment
SavannahOP
Look at this
setCurrentFact() is a state setter functionsaveFact() gets called inside before returning the new index to set the actual state@Savannah `saveFact()` gets called inside before returning the new index to set the actual state
Toyger
you should use setState and saveFact as two different things, it's not a good idea to run it inside sestate
SavannahOP
But I've heard that it's better to use the argument provided by set state function to calculate the next state and not the previous saved version inside component
I'm talking about this
Alright, let me rebuild it without calculating the value inside set state function
It works! This is the final answer 😁
// page.tsx
export default function Home() {
const savedFact = parseInt(cookies().get("latestFact")?.value || "0")
...
return <FactCard savedFact={savedFact}/>
// factCard.tsx
export default function FactCard({ savedFact }: FactProps) {
const [currentFact, setCurrentFact] = useState<number>(savedFact);
function incrementFactBy(amount: number) {
let newIndex;
if (currentFact === facts.length - 1 && amount > 0) {
newIndex = 0;
} else if (currentFact === 0 && amount < 0) {
newIndex = facts.length - 1;
} else {
newIndex = currentFact + amount;
}
setCurrentFact(newIndex)
saveFact(newIndex)
}
return (
<div className={styles.card_wrapper}>
<h3 className="text-center">
Fact about me №{currentFact + 1}/{facts.length}
</h3>
<div className={styles.card}>
<Button onClick={() => incrementFactBy(-1)}>{"<"}</Button>
<p className={styles.fact_text}>{facts[currentFact]}</p>
<Button onClick={() => incrementFactBy(1)}>{">"}</Button>
</div>
</div>
);
}
// saveFact.tsx (Server Action)
"use server"
import { cookies } from "next/headers"
export default async function saveFact(index: number) {
cookies().set("latestFact", index.toString())
}