reactjsreact-hookslocal-storage

LocalStorage with State


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.


Solution

  • 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]);