No tienes acceso a esta clase

¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera

Curso Práctico de React.js

Curso Práctico de React.js

Oscar Barajas Tavares

Oscar Barajas Tavares

Eliminando productos del carrito

26/30
Recursos

Aportes 29

Preguntas 6

Ordenar por:

Los aportes, preguntas y respuestas son vitales para aprender en comunidad. Regístrate o inicia sesión para participar.

Problemas:
1- Warning sobre la key repetida al agregar al carrito el mismo elemento.
2- Si los “productos” agregados son exactamente el mismo, al darle a la X en uno solo, se borran todos los elementos agregados al carrito.

Soluciones:
1- Esto sucede en …/containers/MyOrder.jsx dado que se está usando key={product. id} y pues cuando se agregue al carrito los elementos harán referencia al mismo, o sea React dice como, WTF porque se está renderizando el mismo componente ¿?, el Virtual DOM llora, a React le gustan las keys únicas… Pero es fácil solucionarlo, dado que, map, tiene como su segundo termino, el index del vector de COMPONENTES <OrderItem /> que se está renderizando. Por lo que solo hay que cambiar en esa parte lo siguiente:

{state.cart.map((product,index) => (
		<OrderItem 
			indexValue={index}
			key={index}
			product={product} 
		/>
))}

Si te fijaste, hay dice también indexValue={index}, eso es para solucionar el problema 2.

2-Que se borren todos al querer solo eliminar uno, es porque se estaba utilizando como condicional del filter en useInitialState.js el id del producto, y pues si son iguales, pos todos cumplen con la condición. 😅

Así que solo hay que llevar ese valor de indexValue a OrderItem.jsx, y NO product.

const OrderItem = (props) => {
	// a mi me gusta asi, pero puedes poner lo que esta 	 
        // dentro de { } ahí arriba en vez de props
	const { product, indexValue } = props
	const { removeFromCart } = React.useContext(AppContext)

	const handleRemove = (index) => {
		removeFromCart(index)
	}
	return (
		<div className="OrderItem">
			<figure>
				<img src={product.images[0]} alt={product.title} />
			</figure>
			<p>{product.title}</p>
			<p>{product.price}</p>
			<img 
				src={iconClose} 
				alt="close" 
				onClick={() => handleRemove(indexValue )}
			/>
		</div>
	);

}

export default OrderItem;

Ahí le estamos pasando un indexValue unico del vector que se esta creando en addToCart a handleRemove.

Veamos entonces que pasa con ese filter de useInitialState.js:

  const removeFromCart = (indexValue) => {
    setState({
      ...state,
      cart: state.cart.filter((product,index) => index !== indexValue),
    })
  }

Si, ya se, product queda de “adorno” pero pues filter funciona así, filter(element, index), y nosotros solo necesitamos el index. Ese condicional lo que dice es: “SALVA TODOS” los elementos (<OrderItem />) del vector state.cart que NO tengan ese indexValue que llega. El index que llega por supuesto es al que le hacemos click. Por lo tanto, lo saca del vector y el nuevo vector state.cart es sin ese elemento al que hicimos click en la X.

Gráficamente seria (esto es solo ilustrativo, ni es una sintaxis correcta 😬):

<MyOrder>

<OrderItem
index=0
product=🤡
removeCart(click( if (index=indexValue)))
/>
<OrderItem
index=1
product=🤡
removeCart(click( if (index=indexValue)))
/>
<OrderItem
index=2
product=🤡
removeCart(click( if (index=indexValue)))
/>

</MyOrder>

Besos 😗

Después de pensar un rato la solución más simple que encontré fue pasarle tanto el payload como el index, tal que así:
useInitialState.js

const removeFromCart = (payload, indexValue) => {
    setState({
      ...state,
      cart: state.cart.filter(
        (item, index) => item.id != payload && index != indexValue
      ),
    });
  }; 

Si eliges el mismo elemento varias veces y después eliminas 1, se eliminarán todos porque todos comparten el mismo id.

Como solucion al problema del id, lo que hice fue que basicamente al agregar un item al carrito creo un nuevo id correspondiente a la longitud del carrito, y al removerlo lo remuevo tomando en cuenta el id nuevo asi:

const addToCart = (payload) =>{
        setState({
            ...state,
            cart : [
                ...state.cart,
                {...payload, idCart:state.cart.length+1},
            ]
        })
    }
    const removeFromCart = (payload) => {
        setState({
            ...state,
            cart: state.cart.filter((product)=>product.idCart!==payload.idCart)
        })
    }```

JAJAJAJAJAJA su reacción cuando dice “ok… no funcionó” vale oro

Recomiendo agregar un pointer al icono de borrar del carrito

OrderItem.scss

.OrderItem img {
	cursor: pointer;
}

Si agregamos el mismo elemento, primero, react nos dará el error que OrderItem tiene varios elementos con la misma Key, y si eliminamos uno de estos de estos del carrito también se borraran todos.

Para dar una solución a esto, en product item, yo usé Math.random para crear un id unico para item añadido al carrito y funciona bien.

const handleCart = (product) =>{
    let idItemInCart =  Math.floor(Math.random() * (500000 - 1000)) 
		addToCart({...product,idItemInCart})
	}

Si bien entre 500.000 y 1000 la probabilidad de que repita es poca para la canitdad de elementos que tenemos, sé que esto no es buena practica…

Alguien sabe de alguna mejor forma?

Solución: Bug al eliminar productos repetidos


Para comenzar, el key de cada OrderItem debería estar ligado a su posición en el array de cart para que realmente sea único. Pero los valores de key sirven a React y no son pasados al componente así que necesitamos otro atributo para pasar el valor del index al componente. Esto lo hacemos así:
.
MyOrder.js

<div className="order-products">
	{cart.map( (product, index) => (
            <OrderItem product={product} key={`order-item-${index}`} keyIndex={index} />
          ))}
          
</div>

.
El componente OrderItem recibe el producto y su keyIndex. Pero solo se necesita el keyIndex para disparar la función removeFromCart.
.
OrderItem.js

function OrderItem ({product, keyIndex}) {

  const { removeFromCart } = useContext(AppContext);

  const handleRemove = () => {
    removeFromCart( keyIndex);
  }

  return (
    <div className="orderItem">
      <figure className="orderItem__img">
        <img src={product.images[0]} className="orderItem__img" alt={product.title} />
      </figure>
      <p className="orderItem__name">
        {product.title}
      </p>
      <p className="orderItem__price">
      ${product.price} 
      </p>
      <img src={iconClose} alt="close" onClick={ handleRemove} />
    </div>

  );
}

.
Finalmente, la función de removeFromCart funcionaría con el método splice para cortar un elemento de cart en la posición keyIndex. (recuerda que para usar splice se necesita crear una copia del array a modificar)

    const removeFromCart = (keyIndex) => {
        const newCart = state.cart;
        newCart.splice(keyIndex,1);

        setState({
            ...state,
            cart: newCart
        })
    }

Vamos creando la lógica para crear una función que elimine productos del carrito. Recordemos, que la función para poder añadir la teníamos en nuestro hook, así que ahí mismo vamos a crear una función para eliminar.

// useInitialState
import { useState } from "react";

const useInitialState = () => {
    const removeFromCart = (payload) => {
        setState({// minificado
            cart: state.cart.filter(item => item.id !== payload.id),
        })
    }

    return {
        state,
        addToCart,
        removeFromCart,
    }
}

export default useInitialState;

Dentro del hook, vamos a crear una función llamada removeFromCart. Esta función recibirá como parámetro un payload ( o el producto ), y después actualizaremos el estado. Para ello, con ayuda de setState, indicamos que ahora el estado seguirá siendo un objeto, donde cart, será el array, PERO usaremos el método filter, donde eliminaremos el item del array con ayuda de su id, y después regresará el array sin este elemento. Al final, tenemos que regresar la nueva función en el hook, es decir, añadir return … , removeFromCart.

// OrderItem.jsx
import React, { useContext } from 'react';
import AppContext from '@context/AppContext';

const OrderItem = ({ product }) => {
	const { removeFromCart } = useContext(AppContext);

	const handleRemove = () => {
		removeFromCart(product);
	}

	return (
		<div className="OrderItem">
			<img src={icon_close} alt="close" onClick={() => handleRemove(product)} />
		</div>
	);
}

export default OrderItem;

Recordemos, que el ícono de eliminar, lo tenemos en OrderItem, así que este componente será el encargado de usar la función para remover el producto. Para ello, debemos importar también useContext para poder trabajar con el contexto. Para implementar la función, creamos una constante donde traemos la función removeFromCart del contexto. Después, creamos una función que se encargará de manejar el click sobre el ícono de eliminar. En esta función, siempre ejecutaremos la función removeFromCart con el argumento product, para eventualmente quitarlo de nuestro carrito de compras.

Una manera para manejar el error del id es que pueden agregar una parte que les permita elegir cantidad y ya solo lo multiplican por el costo del producto.

Opinion:
Mencionan muchos que hay un problema a la hora de eliminar los item, pues pueden agregar el mismo y generarse id repetidos. Yo propongo evitar que se pueda agregar item de igual id, pues no tiene sentido agregar 2 veces el mismo elemento, en todo caso deberia asignarse a un mismo elemento una cantidad variable.
Aqui mi codigo para impedir que se agregue elementos de id repetodo

const useInitialState = () =>{
...

    const addToCart = (payload) =>{
        if(!state.cart.includes(payload)){
            setState({
                ...state,
                cart:[...state.cart, payload]
            })
        }
    }
...
}

export default useInitialState;

Para poder agregar solo 1 de por producto y no se repita productos

const addToCart = (payload) => {
        const payloadExist=state.cart.filter((item) => item.id == payload.id);
        if(payloadExist.length==0){
            setState({
                ...state,
               cart: [...state.cart, payload]
            });
        }
    };

Para poder eliminar productos sin ningún problema

const removeFromCart = (payload) => {
        setState({
            ...state,
            cart: state.cart.filter(
                (item) => item.id != payload.id),
        });
    };

no use el Id por el error que todos sabemos, entonces use

	const hora = new Date();

para enviar a ProductItem

{products.map((product, index) => (
	<ProductItem product={product} milisegundos={hora.getMilliseconds()} key={index} />
) )}

y recibirlo por props

const ProductItem = ({product,milisegundos}) => {
	const {addTocart} = useContext(AppContext)

	const handleClick = (item,milisegundos) =>{ 
		item.milisegundos = milisegundos
		addTocart(item)
	}
	return (
		<div className="ProductItem">
			<img src={product.images[0]} alt="" />
			<div className="product-info">
				<div>
					<p>$ {product.price}</p>
					<p>{product.title}</p>
				</div>
				<figure onClick={()=> handleClick(product, milisegundos)}>
					<img src={addCart} alt="" />
				</figure>
			</div>
		</div>
	);
}

antes de enviar agregamso los milisegundos a item

const handleClick = (item,milisegundos) =>{ 
	item.milisegundos = milisegundos
	addTocart(item)
}

y removemos asi

  const removeFromCart = (payload ) =>{
        setstate({
            ...state,
            cart: state.cart.filter(items => items.milisegundos !== payload.milisegundos)
        })
    }

siempre pasamos la declaración de la función al onClick o cualquier otro evento, nunca una ejecución de la función a menos que la ejecución retorne la declaración de una función pero ese ya es otro detalle a considerar

En el bug de los items repetidos, mi solucion fue crearles un “id” unico al momento de agregarlos asi no se confundia el componente al momento de identificarlos.

Acá cree un timestamp para detectar en que punto se fueron agregados al cart.

	const timestamp = new Date().getTime();

	const addToCart = (payload) => {
		setState({
			...state,
			cart: [
				...state.cart,
				{
					...payload,
					timestamp
				}
			]
		})
	}

Y acá estoy evaluando el id y el timestamp para para filtrar los elementos del cart asi esten repetidos.

	const removeFromCart = (payload,timestamp) => {
		setState({
			...state,
			cart: state.cart.filter(item => 
				item.id !== payload && item.timestamp !== timestamp)
		})
	}

Al final mi OrderItem queda tal que asi. Espero les pueda servir!

const OrderItem = ({product,id}) => {
  const { removeFromCart } = useContext(AppContext);
  const {title,price,images,timestamp} = product;

  const handleClick = (id,timestamp) => {
    removeFromCart(id,timestamp)
  }

  return (
    <div className="OrderItem">
      <figure>
        <img
          src={images[0]}
          alt={title}
        />
      </figure>
      <p>{title}</p>
      <p>${price}</p>
      <img src={CloseIcon} alt="close" onClick={() => handleClick(id,timestamp)} />
    </div>
  );
};

creando la función para toggleOrders y no haciéndolo directamente en el onClick, ademas añadiendo un pequeño condicional en los dos toggle, se puede abrir una de las dos pestañas, Menu o MyOrders, y cerrar a la otra en caso que este abierta, para que no haya interferencia entre las dos pestañas

const handleToggle = () => {
		if(toggleOrders) {
			handleToggleOrders();
		}
		setToggle(!toggle);
	}
	const handleToggleOrders = () => {
		if (toggle) {
			handleToggle();
		}
		setToggleOrders(!toggleOrders)		
	}

esto en Header.jsx

Hola amigos les tengo un Hack cuando quieran acceder a sus componentes rapidamente presionen ctrl + colocar el mause sobre su componente y hacer click

una forma sencilla sin modificar mucho el codigo fue en useInitialState modificar el addToCart y removeFromCart de la siguiente manera

  const addToCart = (payload) => {
    setState({
      ...state,
      cart: [...state.cart, { ...payload, idCart: state.cart.length + 1 }]
    });
  };

  const removeFromCart = (payload) => {
    setState({
      ...state,
      cart: state.cart.filter((product) => product.idCart !== payload.idCart),
    });
  }

si bien aparece un alerta que hay aun dos codigo identicos no da ningun problema al momento de agregar y eliminar

Hola Aquí información para profundizar en el funcionamiento del método filter()
https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/Array/filter

Tuve problemas, pero hallé la solución, y la comento aquí por si a alguien le sirve.
en la función removeFromCart en useInitialState es importante considerar que filter por defecto devuelve un array, así que la lógica no debe estar en un array. Esta confusión puede venir porque en addToCart sí usamos un array en cart.

solucionado hasta aqui, dejo mi repo por si le sirve a alguien…

React-Shop

Haciendo el reto de Bughuters que cierra el modal con la flecha del menu del carrito me di cuenta que realmente no entendi nada de React Context en adelante y ahora revisando el error de eliminar un producto duplicado me doy cuenta que ahora si que menos entiendo que esta pasando😦

Aquí dejo una posible solución a los siguientes posibles errores en la aplicación y adicional a los que usaron el totalPrice como variable global del estado en el useInitialState del video anterior:

  • Se repiten las keys de los elementos al listarse en MyOrder si se selecciona el mismo varias veces
  • Al eliminarse un producto se eliminan todos los que tengan el mismo id
  • El precio no se actualiza, pues no conocemos el elemento que se elimina desde el método filter()

Primero lo que necesitamos hacer es aprovecharnos del índice que nos da el método map() al listar los productos y utilizarlo como key

state.cart.map((product, idx) => {
	//Aqui colocamos al finalizar idx como adicion a la key para que no se repita
	<OrderItem
		idx={idx}
		product={product}
		key={`order-item-${product.id}-${idx}`}
	/>
})

Luego en nuestro OrderItem recibimos el idx como parámetro y se lo pasamos al removeFromCart()

// Aqui lo recibimos junto al producto
const OrderItem = ({ product, idx }) => {
	const { removeFromCart } = useContext(AppContext);

	return (
		<div className="OrderItem">
			<figure>
				<img src={product.images[0]} alt={product.title} />
			</figure>
			<p>{ product.title }</p>
			<p>{ product.price }</p>
			<!-- Aqui en el handler se lo enviamos a nuestra funcion -->
			<img src={CloseIcon} alt="close" onClick={() => removeFromCart(idx)}/>
		</div>
	);
}

Por último modificamos nuestra función en el useInitialState()

Nos Apoyamos en la funcion splice
Esta elimina “x” cantidad de elementos de un array dada una posición inicial y retorna uno nuevo con los elementos eliminados.
En nuestro caso la posición inicial es el idx y solo queremos eliminar un elemento, por eso tomamos el valor [0] del array

const removeFromCart = (idx) => {
	const removedElement = state.cart.splice(idx, 1)[0]

	setState({
		...state,
		cart: state.cart,
		totalPrice: state.totalPrice - removedElement.price
	});
};

Esto se puede mejorar creando un nuevo array de lo que quedó después de haber hecho el splice, pero creo que por ahora es funcional.

Para que el carrito y myOrder bajen mientras hacemos scroll.
en header.scss

.navbar-shopping-cart {
	// position: relative;
	position: fixed;
	right: 10px;
}

en MyOrder.scss

.MyOrder{
	position: fixed;
}

Respecto del problema con tener varios elementos dentro del array con el mismo id, es decir, repetido el producto, se pueden emplear varias cosas.
.
Primero hay qué preguntarse cuál es la lógica de negocio. ¿Es correcto tener un mismo producto dos o más veces? 🤔, si la respuesta es NO entonces no lo deberíamos permitir.
.
En ese caso, podemos solucionarlo de la siguiente manera:

//En la función addToCar primero hacemos una validación, en caso de que se cumpla, enviamos un alert al usuario.
if(carrito.find(item => item.id === producto.id)){
      alert('Ya existe este elemento')
      return
    } 

.
Ahora bien, puede que sí sea necesario poder permitir al usuario escoger la cantidad de un producto. Pero para eso no quedaría bien usar en una misma lista un producto repetido, sino dar la opción de escoger la cantidad que desea adquirir (en este escenario no generé code porque es un poco más largo)…
.
Por otro lado, si queremos dejar el mismo funcionamiento de permitir agregar elementos repetidos, únicamente debemos darles un ID que sea ÚNICO. En este caso podríamos crear una función que nos genere esa id y luego agregarlo al producto al momento de ejecutar addToCar:
.

//Funcion para generar un id UNICO

const generarId = () => {
  const random = Math.random().toString(36).substring(2);
  const fecha  = Date.now().toString(36);
  return random + fecha;
}

//Dificilmente esto se pueda repetir pero puede seguir agregando más cositas

//Agregarle una propiedad al objeto  y que sea nuestro identificador único
producto.dateAdd = generarId()

.
Una vez agregada la propiedad dateAdd, al momento de filtrar cuando vayamos a eliminar, en vez de hacerlo por la propiedad id del producto, lo hacemos por .dateAdd y con eso podemos seguis trabajando perfectamente.
.
Finalmente, es por esto importante saber cuáles son nuestros requerimientos, ya que con ello podremos aplicar la lógica necesaria para satisfacer correctamente con la aplicación.

sí,sí,sí,; los errores son nuestros amigos!!!

Yo le añadí un color al icono de borrar el producto, cuando pasemos el mouse por encima .

.imgClose:hover{
	filter: invert(59%) sepia(95%) saturate(404%) hue-			 
         rotate(77deg) brightness(94%) contrast(100%);
	cursor: pointer;
	-webkit-filter: invert(59%) sepia(95%) saturate(404%) 							 
        hue-rotate(77deg) brightness(94%) contrast(100%);
} 

este fue el recurso que use para hacer el filtro al ICONO y no tener que hacerlo en Photoshop o algo así 😄

Recurso: https://codepen.io/sosuke/pen/Pjoqqp

Que tal campeon, buen dia…
Le agregue un poco de estilo…

Al darle click al carrito, aparece el banner con la ‘x’ de cerrar.

archivo Header.jsx
{!toggleOrders ? (
            <li
              className="navbar-shopping-cart"
              onClick={showBanner}
            >
              <img src={shoppingCart} alt="shopping cart" />
              {state.cart.length > 0 ? <div>{state.cart.length}</div> : null}
            </li>
          ) : (
            <img id="btn-close" src={close} alt="" onClick={closeBanner} />
          )}

funciones de mostrar y cerrar banner.

 const showBanner = () => {
    setToggleOrders(true);
  };

  const closeBanner = () => {
    setToggleOrders(false);
  };

yo solucione el problema del key repetido usando un numero random al renderizar:

  {state.cart.map((product) => (
          <OrderItem
            product={product}
            key={`orderItem-${Math.floor(Math.random() * product.id)}`}
          />
        ))}