How do I save the items of a shopping cart in the local storage?

Hello, I’m new here and I’m going to introduce myself separately. Now to the matter at hand:
I would like to store the items of my webshop, which I created with React and an online tutorial, in the local storage of the browser. I’ve heard that this is a possible way to not lose everything on refresh.
But my question is how this can be done. I’m not good and or experienced enough to implement this feature myself.
I would be grateful for every hint and help.

PS: May I post a link to my project on github?

use localStorage.setItem(myItmes, 'xyz') to store and localStorage.getItem(myItmes) to get them back, note that you need to parse your results using JSON.parse() if you have javascript objects stored in your local storage when you retrieve them, and when you store javascript objects use JSON.stringify()

1 Like

Thank you for the answer. So, in my project, where exactly would I implement this? I have a context.js with the core logic of my project. But still I am unsure where exactly (which function) to do this and how it is done in my specific case. This is the context.js:

import React, { Component } from 'react';
import {storeProducts, detailProduct} from './data';

const ProductContext = React.createContext();
//Provider
//Consumer

class ProductProvider extends Component {
  state = {
      products: [],
      detailProduct: detailProduct,
      cart: [],
      modalOpen: false,
      modalProduct: detailProduct,
      cartSubTotal: 0,
      cartTax: 0,
      cartTotal: 0
  };

  componentDidMount() {
    this.setProducts();
  };

  setProducts = () => {
    let tempProducts = [];
    storeProducts.forEach(item => {
      const singleItem = { ...item };
      tempProducts = [...tempProducts, singleItem];
    })
    this.setState(() => {
      return {products: tempProducts}
    })
  };

  getItem = (id) => {
    const product = this.state.products.find(item => item.id === id);
    return product;
  };

  handleDetail = (id) => {
      const product = this.getItem(id);
      this.setState(() => {
        return {detailProduct:product}
      })
  };

  addToCart = (id) => {
      let tempProducts = [...this.state.products];
      const index = tempProducts.indexOf(this.getItem(id));
      const product = tempProducts[index];
      product.inCart = true;
      product.count = 1;
      const price = product.price;
      product.total = price;
      this.setState(() => {
        return {products:tempProducts, cart:[...this.state.cart,
        product] };
      }, () => { this.addTotals(); });
  };

  openModal = (id) => {
    const product = this.getItem(id);
    this.setState(() => {
      return {modalProduct:product, modalOpen:true}
    });
  };

  closeModal = () => {
    this.setState(() => {
      return {modalOpen:false}
    });
  };

  increment = (id) => {
    let tempCart = [...this.state.cart];
    const selectedProduct = tempCart.find(item => item.id === id);

    const index = tempCart.indexOf(selectedProduct);
    const product = tempCart[index];

    product.count = product.count + 1;
    product.total = product.count * product.price;

    this.setState(() => {
      return{cart: [...tempCart]}
    }, () => {
      this.addTotals()
    })
  };

  decrement = (id) => {
    let tempCart = [...this.state.cart];
    const selectedProduct = tempCart.find(item => item.id === id);

    const index = tempCart.indexOf(selectedProduct);
    const product = tempCart[index];

    product.count = product.count - 1;

    if(product.count === 0) {
      this.removeItem(id);
    }
    else {
      product.total = product.count * product.price;
    }

    this.setState(() => {
      return{cart: [...tempCart]}
    }, () => {
      this.addTotals()
    })
  };

  removeItem = (id) => {
    let tempProducts = [...this.state.products];
    let tempCart = [...this.state.cart];

    tempCart = tempCart.filter(item => item.id !== id);

    const index = tempProducts.indexOf(this.getItem(id));
    let removedProduct = tempProducts[index];
    removedProduct.inCart = false;
    removedProduct.count = 0;
    removedProduct.total = 0;

    this.setState(() => {
      return {
        cart: [...tempCart],
        products: [...tempProducts]
      }
    }, () => {
      this.addTotals();
    })
  };

  clearCart = () => {
    this.setState(() => {
      return { cart: [] }
    }, () => {
      this.setProducts();
      this.addTotals();
    });
  };

  addTotals = () => {
    let subTotal = 0;
    this.state.cart.map(item => (subTotal += item.total));
    const tempTax = subTotal * 0.1;
    const tax = parseFloat(tempTax.toFixed(2));
    const total = subTotal + tax;
    this.setState(() => {
      return {
        cartSubTotal:subTotal,
        cartTax:tax,
        cartTotal:total
      }
    })
  };

  render() {
    return (
      <ProductContext.Provider 
        value={{
          ...this.state,
          handleDetail: this.handleDetail,
          addToCart: this.addToCart,
          openModal: this.openModal,
          closeModal: this.closeModal,
          increment: this.increment,
          decrement: this.decrement,
          removeItem: this.removeItem,
          clearCart: this.clearCart
        }}>
        {this.props.children}
      </ProductContext.Provider>
    )
  }
}

const ProductConsumer = ProductContext.Consumer;

export {ProductProvider, ProductConsumer};

Depends, I don’t know the details of your project but typically I like to store on setState() callbacks, for instance, when you add to your cart you could, …

  addToCart = (id) => {
      let tempProducts = [...this.state.products];
      const index = tempProducts.indexOf(this.getItem(id));
      const product = tempProducts[index];
      product.inCart = true;
      product.count = 1;
      const price = product.price;
      product.total = price;
      this.setState(() => {
        return {products:tempProducts, cart:[...this.state.cart,
        product] };
      }, () => { 
               this.addTotals();
               localStorage.setItem('myCart', JSON.stringify(this.state.cart))
               });
  };

and you can just load your items when your component mounts, example in componentDidMount(), you could…

  componentDidMount() {
    this.setProducts();
    this.setState({cart : JSON.parse(localStorage.getItem('myCart'))})
  };

I think some linters complain when you set state on CDM, so you could move it to it’s own function, anyhow , you’d do something like the above but of course there are many ways to skin the cat, the above is just one way, like I said above it depends on how your overall app functions

TypeError: Invalid attempt to spread non-iterable instance

_nonIterableSpread

webshop-master/node_modules/@babel/runtime/helpers/esm/nonIterableSpread.js:2

   1 | export default function _nonIterableSpread() {
 > 2 |   throw new TypeError("Invalid attempt to spread non-iterable instance");  
   3 | }

View compiled

_toConsumableArray

webshop-master/node_modules/@babel/runtime/helpers/esm/toConsumableArray.js:5

   2 | import iterableToArray from "./iterableToArray";  
   3 | import nonIterableSpread from "./nonIterableSpread";  
   4 | export default function _toConsumableArray(arr) {
 > 5 |   return arrayWithoutHoles(arr) || iterableToArray(arr) || nonIterableSpread();  
   6 | }

View compiled

ProductProvider.<anonymous>

webshop-master/src/context.js:57

   54 | const price = product.price;  
   55 | product.total = price;  
   56 | this.setState(() => {
 > 57 |   return { products:tempProducts, cart:[...this.state.cart,     | ^  
   58 |   product] };  
   59 | }, () => {   60 |           this.addTotals(); 

where’s your new code ? and was it working before you tried to do a localStorage?

componentDidMount() {
    this.setProducts();
    this.setState({cart :JSON.parse(localStorage.getItem('myCart'))})
  };

addToCart = (id) => {
      let tempProducts = [...this.state.products];
      const index = tempProducts.indexOf(this.getItem(id));
      const product = tempProducts[index];
      product.inCart = true;
      product.count = 1;
      const price = product.price;
      product.total = price;
      this.setState(() => {
        return {products:tempProducts, cart:[...this.state.cart,
        product] };
      }, () => { this.addTotals(); });
  };

  saveCart = () => {
    localStorage.setItem('myCart', JSON.stringify(this.state.cart))
  };

Yes, it works without the new code.

Where are you calling saveCart() ? , try calling it before or after addTotals() after the state is set like I did above then in chrome in your console go to Application > Local Storage > and see it your cart is saved. if there is no cart then you are not saving it properly, start from there…, remove the logic from componentDidMount() for now.

Ok, I removed

this.setState({cart :JSON.parse(localStorage.getItem('myCart'))})

from componentDidMount() and added localStorage.setItem to addToCart() again and I get no errors so far. Also, myCart is indeed saved to the local storage.

ok, so the problem is that on CDM, if your cart in your local storage is undefined , say a user is using your app for the first time for instance, then there would be nothing to set the state with therefore you could try a conditional like so

componentDidMount() {
    this.setProducts();
    this.setState({cart : !localStorage.getItem('myCart') 
                          ? []
                          : JSON.parse(localStorage.getItem('myCart'))
                  })
    };

basically you would be telling it to set your cart items on CDM only if there are indeed items in your local storage

It works now! Thank you very much for your patience and the solution to my problem.

yeah, it works now because you already saved that item in your storage and it’s not empty, but if you apply the above code and clear your storage it should still work, however it won’t work if you clear your storage but don’t include the above snippet

Another thing occurred:

Namely, when I refresh the page, the prices are set back to zero. They also don’t appear in the local storage.
What I tried is to do localStorage.setItem('myCart', JSON.stringify(this.state.cart, this.state.cartSubtotal...))
among other things. Could you help me figure this one out, too?

JSON.stringify() takes only 1 required argument and 2 optional ones, it thinks that your this.state.cartSubtotal is the first optional argument when it isn’t, either a) separately save your storage items or b) unify the object you want to store into one before saving it.

a)

localStorage.setItem('myCart', JSON.stringify(this.state.cart))
localStorage.setItem('myTotal', JSON.stringify(this.state.cart))

b)

const unified = Object.assign({}, {cart: this.state.cart}, {subtotal: this.state.cartSubtotal})
localStorage.setItem('myObject', JSON.stringify(unified))


I did this:

addToCart = (id) => {
      let tempProducts = [...this.state.products];
      const index = tempProducts.indexOf(this.getItem(id));
      const product = tempProducts[index];
      product.inCart = true;
      product.count = 1;
      const price = product.price;
      product.total = price;
      this.setState(() => {
        return {products:tempProducts, cart:[...this.state.cart,
        product] };
      }, () => { 
                 this.addTotals();
                 const unified = Object.assign({}, {cart: this.state.cart}, {subTotal: this.state.cartSubtotal})
                 localStorage.setItem('myObject', JSON.stringify(unified))
               });
  };

and in the componentDidMount:

componentDidMount() {
    this.setProducts();
    this.setState({cart : !localStorage.getItem('myObject') 
                          ? []
                          : JSON.parse(localStorage.getItem('myObject'))
                  })
  };

But now I did something wrong again for I get the error: Invalid attempt to spread non-iterable instance
again.

probably because this.state.cart is an array, you have a general direction on how to handle the problem, google around and see how to properly unify the objects, stringify them and then store the unified object, if you still have problems, post what you have tried here…

componentDidMount() {
    this.setProducts();
    this.setState({unified : !localStorage.getItem('myObject') 
                          ? []
                          : JSON.parse(localStorage.getItem('myObject'))
                  })
  };


addToCart = (id) => {
      let tempProducts = [...this.state.products];
      const index = tempProducts.indexOf(this.getItem(id));
      const product = tempProducts[index];
      product.inCart = true;
      product.count = 1;
      const price = product.price;
      product.total = price;
      this.setState(() => {
        return {products:tempProducts, cart:[...this.state.cart,
        product] };
      }, () => { 
                 this.addTotals();
                 const unified = Object.assign({}, {cart: this.state.cart}, {subTotal: this.state.cartSubTotal})
                 localStorage.setItem('myObject', JSON.stringify(unified))
               });
  };

With this code I get a subtotal in the local storage but when I refresh the products are gone.
I guess the mistake is somewhere in the componentDidMount?

Well, think this thru again, in your original idea you wanted to save the cart contents in the local storage. So, once stored correctly, in your componentDidMount() you set the state of cart with the what was stored in the browser and it worked, now you want to also store the carTotal as well, assuming that you have correctly stored one unified object consisting of the carTotal and cart in the browser’s local storage, what state properties would you want to retrieve from the local storage and update your component with?

1 Like

May I recommend not using localstorage for your shopping cart. By default, apple tablet does not allow you to use localstorage unless you turn it on. That would mean you’d be missing out on all customers who use apple tablets.

I’m not sure if Apple updated this,. but this was the case 7-8 months ago.

My original idea was / is to show the product (which worked) and the 3 amounts (subTotal, tax and total) in the cart and not losing it on refresh.