From 76f5ed503064f9574cbb58f4875e288ff33a7dc8 Mon Sep 17 00:00:00 2001 From: Rua Haszard Date: Mon, 9 Mar 2020 15:09:47 +1300 Subject: [PATCH] improve responsiveness when setting cart item quantities (https://github.com/woocommerce/woocommerce-blocks/pull/1864) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * rework the quantity change generator action so the UI updates quick: - work in progress - still need to figure out how to debounce API call - add new action for updating quantity for an item - don't set cart item as pending while quantity is updating - this leaves QuantitySelector enabled so user can click more/less - use receiveCartItemQuantity to update quantity in UI before sending request * debounce line item quantity first cut: - use local state for quantity, so ui allows multiple clicks up/down - debounce store updates (and server/API call) * correct comment on cart item quantity reducer * remove recieveCartItemQuantity - no longer needed * remove delegation for deleted RECEIVE_CART_ITEM_QUANTITY * only update quantity in component sideffect if it has changed: - reduces unnecessary renders * factor out debounced quantity update into cartItem hook (hat tip @senadir) * use quantity from store, instead of passing in to hook + + fix latent bug in useStoreCartItem - the cartItem value is now object: - was previously single-item array - (note no client code is using this at present) * tidy/refactor cart item hook - separate dispatch from select * remove dud reset of item pending flag (came back in rebase) * add quantity to StoreCartItem hook return value typedef * fix js error when adding cart block in editor – cartItem not found * fix typedef * fix logic for debouncing * don’t update quantity on server unnecessarily Co-authored-by: Darren Ethier --- .../js/base/hooks/use-store-cart-item.js | 70 +++++++++++++------ .../cart/full-cart/cart-line-item-row.js | 15 ++-- .../assets/js/data/cart/actions.js | 13 ++-- .../assets/js/type-defs/hooks.js | 7 +- 4 files changed, 67 insertions(+), 38 deletions(-) diff --git a/plugins/woocommerce-blocks/assets/js/base/hooks/use-store-cart-item.js b/plugins/woocommerce-blocks/assets/js/base/hooks/use-store-cart-item.js index 96328180c59..537a7f42235 100644 --- a/plugins/woocommerce-blocks/assets/js/base/hooks/use-store-cart-item.js +++ b/plugins/woocommerce-blocks/assets/js/base/hooks/use-store-cart-item.js @@ -3,8 +3,10 @@ /** * External dependencies */ -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { useState, useEffect } from '@wordpress/element'; import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data'; +import { useDebounce } from 'use-debounce'; /** * Internal dependencies @@ -22,32 +24,60 @@ import { useStoreCart } from './use-store-cart'; */ export const useStoreCartItem = ( cartItemKey ) => { const { cartItems, cartIsLoading } = useStoreCart(); - const cartItem = cartItems.filter( ( item ) => item.key === cartItemKey ); - - const results = useSelect( - ( select, { dispatch } ) => { + const [ cartItem, setCartItem ] = useState( { + key: '', + isLoading: true, + cartData: {}, + quantity: 0, + isPending: false, + changeQuantity: () => void null, + removeItem: () => void null, + } ); + // Store quantity in hook state. This is used to keep the UI + // updated while server request is updated. + const [ quantity, changeQuantity ] = useState( cartItem.quantity ); + const [ debouncedQuantity ] = useDebounce( quantity, 400 ); + const isPending = useSelect( + ( select ) => { const store = select( storeKey ); - const isPending = store.isItemQuantityPending( cartItemKey ); - const { removeItemFromCart, changeCartItemQuantity } = dispatch( - storeKey - ); - - return { - isPending, - changeQuantity: ( newQuantity ) => { - changeCartItemQuantity( cartItemKey, newQuantity ); - }, - removeItem: () => { - removeItemFromCart( cartItemKey ); - }, - }; + return store.isItemQuantityPending( cartItemKey ); }, [ cartItemKey ] ); + useEffect( () => { + if ( ! cartIsLoading ) { + const foundCartItem = cartItems.find( + ( item ) => item.key === cartItemKey + ); + if ( foundCartItem ) { + setCartItem( foundCartItem ); + } + } + }, [ cartItems, cartIsLoading, cartItemKey ] ); + + const { removeItemFromCart, changeCartItemQuantity } = useDispatch( + storeKey + ); + const removeItem = () => { + removeItemFromCart( cartItemKey ); + }; + + // Observe debounced quantity value, fire action to update server when it + // changes. + useEffect( () => { + if ( debouncedQuantity === 0 ) { + changeQuantity( cartItem.quantity ); + return; + } + changeCartItemQuantity( cartItemKey, debouncedQuantity ); + }, [ debouncedQuantity, cartItemKey, cartItem.quantity ] ); return { + isPending, + quantity, + changeQuantity, + removeItem, isLoading: cartIsLoading, cartItem, - ...results, }; }; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout/cart/full-cart/cart-line-item-row.js b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout/cart/full-cart/cart-line-item-row.js index 8c648dc4bf8..78712a9ad07 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout/cart/full-cart/cart-line-item-row.js +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout/cart/full-cart/cart-line-item-row.js @@ -28,21 +28,21 @@ const CartLineItemRow = ( { lineItem = {} } ) => { permalink = '', images = [], variation = [], - quantity = 1, prices = {}, } = lineItem; + const { + quantity, + changeQuantity, + removeItem, + isPending: itemQuantityDisabled, + } = useStoreCartItem( key ); + const currency = getCurrency(); const regularPrice = parseInt( prices.regular_price, 10 ) * quantity; const purchasePrice = parseInt( prices.price, 10 ) * quantity; const saleAmount = regularPrice - purchasePrice; - const { - changeQuantity, - removeItem, - isPending: itemQuantityDisabled, - } = useStoreCartItem( key ); - return ( @@ -121,7 +121,6 @@ CartLineItemRow.propTypes = { name: PropTypes.string.isRequired, summary: PropTypes.string.isRequired, images: PropTypes.array.isRequired, - quantity: PropTypes.number.isRequired, low_stock_remaining: PropTypes.number, sold_individually: PropTypes.bool, variation: PropTypes.arrayOf( diff --git a/plugins/woocommerce-blocks/assets/js/data/cart/actions.js b/plugins/woocommerce-blocks/assets/js/data/cart/actions.js index 29cb35c521d..2be4a9f5c33 100644 --- a/plugins/woocommerce-blocks/assets/js/data/cart/actions.js +++ b/plugins/woocommerce-blocks/assets/js/data/cart/actions.js @@ -1,12 +1,13 @@ /** * External dependencies */ -import { apiFetch } from '@wordpress/data-controls'; +import { apiFetch, select } from '@wordpress/data-controls'; /** * Internal dependencies */ import { ACTION_TYPES as types } from './action-types'; +import { STORE_KEY as CART_STORE_KEY } from './constants'; /** * Returns an action object used in updating the store with the provided items @@ -214,18 +215,20 @@ export function* removeItemFromCart( cartItemKey ) { } /** - * Changes the quantity for specified cart item: + * Persists a quantity change the for specified cart item: * - Calls API to set quantity. * - If successful, yields action to update store. * - If error, yields action to store error. - * - Sets cart item as pending while API request is in progress. * * @param {string} cartItemKey Cart item being updated. * @param {number} quantity Specified (new) quantity. */ export function* changeCartItemQuantity( cartItemKey, quantity ) { - yield itemQuantityPending( cartItemKey, true ); + const cartItem = yield select( CART_STORE_KEY, 'getCartItem', cartItemKey ); + if ( cartItem?.quantity === quantity ) { + return; + } try { const cart = yield apiFetch( { path: `/wc/store/cart/update-item`, @@ -241,8 +244,6 @@ export function* changeCartItemQuantity( cartItemKey, quantity ) { } catch ( error ) { yield receiveError( error ); } - - yield itemQuantityPending( cartItemKey, false ); } /** diff --git a/plugins/woocommerce-blocks/assets/js/type-defs/hooks.js b/plugins/woocommerce-blocks/assets/js/type-defs/hooks.js index f657d392dd8..4028a7871a8 100644 --- a/plugins/woocommerce-blocks/assets/js/type-defs/hooks.js +++ b/plugins/woocommerce-blocks/assets/js/type-defs/hooks.js @@ -33,13 +33,12 @@ * * @property {boolean} isLoading True when cart items are being * loaded. - * @property {CartData} cartData A cart item from the data store. - * @property {Function} isPending Callback for determining if a cart - * item is currently updating (i.e. - * remove / change quantity). + * @property {number} quantity The quantity of the item in the cart. + * @property {boolean} isPending Whether the cart item is updating or not. * @property {Function} changeQuantity Callback for changing quantity of item * in cart. * @property {Function} removeItem Callback for removing a cart item. + * @property {Object} cartItem The cartItem retrieved. */ export {};