I'm using the following code to store cart data into local storage
"use client";
import React, { useState, useEffect } from "react";
export default function Home() {
const [cartItems, setCartItems] = useState([]);
useEffect(() => {
const cartItemsData = JSON.parse(localStorage.getItem("cartItems"));
if (cartItemsData) {
console.log("loaded cart items data", cartItemsData);
setCartItems(cartItemsData);
}
}, []);
useEffect(() => {
console.log("storing cart items", JSON.stringify(cartItems));
localStorage.setItem("cartItems", JSON.stringify(cartItems));
}, [cartItems]);
return <div>{JSON.stringify(cartItems)}</div>;
}
The issue I'm having is that when the page reloads, there is a race condition as it calls the use effect hook which pulls the data from local storage and after it calls the use effect hook with cartItems=[] from the initialisation of useState([]) which then wipes local storage and sets cartItems to [].
I tried loading the data from local storage in useState but I get an error as the window hasn't loaded at that point.
How do I fix this?
Interestingly if I set useState(""), it seems to work ok as in this case, the setCartItems is called after the initial use effect fires.
When you initialize the cartItems's state with an empty array ([])
, the useEffect
that read from localStorage
will run after the initial render. This means the initial state will be an empty array, and the useEffect
that writes to localStorage
will immediately store this empty array before the useEffect
that reads from localStorage
can update the state with the actual data.
On the other hand, when you initialize the cartItems's state with an empty string ("")
, the initial state is not an empty array, so it do not trigger the same immediate overwrite behavior. This can cause the useEffect
that reads from localStorage
to run and set the state before the useEffect
that writes to localStorage
can overwrite it.
The solutiuon can be to initialize state with localStorage
data or an empty array
if no data is found.
const [cartItems, setCartItems] = useState(() => {
// Check if running in the client
if (typeof window !== 'undefined') {
const cartItemsData = localStorage.getItem("cartItems");
return cartItemsData ? JSON.parse(cartItemsData) : [];
}
return [];
});
Alternatively, we can mimic componentDidMount
behavior in this functional component like:
const [cartItems, setCartItems] = useState([]);
const [isMounted, setIsMounted] = useState(false);
// Mimic componentDidMount
useEffect(() => {
setIsMounted(true);
// Load the data from localStorage if exists
const cartItemsData = localStorage.getItem("cartItems");
if (cartItemsData) {
console.log("loaded cart items data", JSON.parse(cartItemsData));
setCartItems(JSON.parse(cartItemsData));
}
}, []);
// Sync cartItems to localStorage whenever it changes but only if the component has been mounted.
useEffect(() => {
if (isMounted) {
console.log("storing cart items", JSON.stringify(cartItems));
localStorage.setItem("cartItems", JSON.stringify(cartItems));
}
}, [cartItems, isMounted]);